New SQLite 3 state store

Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
ItalyPaleAle 2023-01-24 14:39:03 -08:00
parent 570f67b797
commit 6f71753c13
12 changed files with 1789 additions and 8 deletions

View File

@ -88,6 +88,7 @@ jobs:
- state.postgresql
- state.redis.v6
- state.redis.v7
- state.sqlite
- state.sqlserver
- state.in-memory
- state.cockroachdb

16
go.mod
View File

@ -127,6 +127,7 @@ require (
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
k8s.io/utils v0.0.0-20230115233650-391b47cb4029
modernc.org/sqlite v1.20.3
)
require (
@ -188,6 +189,7 @@ require (
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dubbogo/gost v1.13.1 // indirect
github.com/dubbogo/triple v1.1.8 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/eapache/go-resiliency v1.3.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect
@ -261,6 +263,7 @@ require (
github.com/k0kubun/pp v3.0.1+incompatible // indirect
github.com/kataras/go-errors v0.0.3 // indirect
github.com/kataras/go-serializer v0.0.4 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.14 // indirect
github.com/knadh/koanf v1.4.1 // indirect
github.com/kubemq-io/protobuf v1.3.1 // indirect
@ -280,7 +283,7 @@ require (
github.com/matryer/is v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
github.com/miekg/dns v1.1.43 // indirect
@ -315,6 +318,7 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/statsd_exporter v0.22.7 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/rs/zerolog v1.25.0 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
@ -354,6 +358,7 @@ require (
golang.org/x/term v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.2.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
@ -373,6 +378,15 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect

36
go.sum
View File

@ -1038,6 +1038,7 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -1269,6 +1270,8 @@ github.com/kataras/go-errors v0.0.3 h1:RQSGEb5AHjsGbwhNW8mFC7a9JrgoCLHC8CBQ4keXJ
github.com/kataras/go-errors v0.0.3/go.mod h1:K3ncz8UzwI3bpuksXt5tQLmrRlgxfv+52ARvAu1+I+o=
github.com/kataras/go-serializer v0.0.4 h1:isugggrY3DSac67duzQ/tn31mGAUtYqNpE2ob6Xt/SY=
github.com/kataras/go-serializer v0.0.4/go.mod h1:/EyLBhXKQOJ12dZwpUZZje3lGy+3wnvG7QKaVJtm/no=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -1370,9 +1373,11 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@ -1615,6 +1620,8 @@ github.com/rabbitmq/amqp091-go v1.6.0/go.mod h1:wfClAtY0C7bOHxd3GjmF26jEHn+rR/0B
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
@ -2213,6 +2220,7 @@ golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -2323,6 +2331,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -2677,6 +2687,30 @@ k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+O
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556WaVQylDXswKmX+dE=
k8s.io/utils v0.0.0-20230115233650-391b47cb4029/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -300,16 +300,14 @@ func (p *PostgresDBAccess) Get(parentCtx context.Context, req *state.GetRequest)
}
return &state.GetResponse{
Data: data,
ETag: ptr.Of(strconv.FormatUint(uint64(etag), 10)),
Metadata: req.Metadata,
Data: data,
ETag: ptr.Of(strconv.FormatUint(uint64(etag), 10)),
}, nil
}
return &state.GetResponse{
Data: value,
ETag: ptr.Of(strconv.FormatUint(uint64(etag), 10)),
Metadata: req.Metadata,
Data: value,
ETag: ptr.Of(strconv.FormatUint(uint64(etag), 10)),
}, nil
}

128
state/sqlite/sqlite.go Normal file
View File

@ -0,0 +1,128 @@
/*
Copyright 2022 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sqlite
import (
"context"
"reflect"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/components-contrib/state"
"github.com/dapr/kit/logger"
)
// SQLite Database state store.
type SQLiteStore struct {
logger logger.Logger
dbaccess DBAccess
}
// NewSQLiteStateStore creates a new instance of the SQLite state store.
func NewSQLiteStateStore(logger logger.Logger) state.Store {
dba := newSqliteDBAccess(logger)
return newSQLiteStateStore(logger, dba)
}
// newSQLiteStateStore creates a newSQLiteStateStore instance of an Sqlite state store.
// This unexported constructor allows injecting a dbAccess instance for unit testing.
func newSQLiteStateStore(logger logger.Logger, dba DBAccess) *SQLiteStore {
return &SQLiteStore{
logger: logger,
dbaccess: dba,
}
}
// Init initializes the Sql server state store.
func (s *SQLiteStore) Init(metadata state.Metadata) error {
return s.dbaccess.Init(metadata)
}
func (s SQLiteStore) GetComponentMetadata() map[string]string {
metadataStruct := sqliteMetadataStruct{}
metadataInfo := map[string]string{}
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo)
return metadataInfo
}
// Features returns the features available in this state store.
func (s *SQLiteStore) Features() []state.Feature {
return []state.Feature{
state.FeatureETag,
state.FeatureTransactional,
}
}
func (s *SQLiteStore) Ping() error {
return s.dbaccess.Ping(context.TODO())
}
// Delete removes an entity from the store.
func (s *SQLiteStore) Delete(ctx context.Context, req *state.DeleteRequest) error {
return s.dbaccess.Delete(ctx, req)
}
// BulkDelete removes multiple entries from the store.
func (s *SQLiteStore) BulkDelete(ctx context.Context, req []state.DeleteRequest) error {
ops := make([]state.TransactionalStateOperation, len(req))
for i, r := range req {
ops[i] = state.TransactionalStateOperation{
Operation: state.Delete,
Request: r,
}
}
return s.dbaccess.ExecuteMulti(ctx, ops)
}
// Get returns an entity from store.
func (s *SQLiteStore) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) {
return s.dbaccess.Get(ctx, req)
}
// BulkGet performs a bulks get operations.
func (s *SQLiteStore) BulkGet(ctx context.Context, req []state.GetRequest) (bool, []state.BulkGetResponse, error) {
// TODO: replace with ExecuteMulti for performance.
return false, nil, nil
}
// Set adds/updates an entity on store.
func (s *SQLiteStore) Set(ctx context.Context, req *state.SetRequest) error {
return s.dbaccess.Set(ctx, req)
}
// BulkSet adds/updates multiple entities on store.
func (s *SQLiteStore) BulkSet(ctx context.Context, req []state.SetRequest) error {
ops := make([]state.TransactionalStateOperation, len(req))
for i, r := range req {
ops[i] = state.TransactionalStateOperation{
Operation: state.Upsert,
Request: r,
}
}
return s.dbaccess.ExecuteMulti(ctx, ops)
}
// Multi handles multiple transactions. Implements TransactionalStore.
func (s *SQLiteStore) Multi(ctx context.Context, request *state.TransactionalStateRequest) error {
return s.dbaccess.ExecuteMulti(ctx, request.Operations)
}
// Close implements io.Closer.
func (s *SQLiteStore) Close() error {
if s.dbaccess != nil {
return s.dbaccess.Close()
}
return nil
}

View File

@ -0,0 +1,500 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sqlite
import (
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/google/uuid"
// Blank import for the underlying SQLite Driver.
_ "modernc.org/sqlite"
"github.com/dapr/components-contrib/state"
stateutils "github.com/dapr/components-contrib/state/utils"
"github.com/dapr/kit/logger"
)
// DBAccess is a private interface which enables unit testing of SQLite.
type DBAccess interface {
Init(metadata state.Metadata) error
Ping(ctx context.Context) error
Set(ctx context.Context, req *state.SetRequest) error
Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error)
Delete(ctx context.Context, req *state.DeleteRequest) error
ExecuteMulti(ctx context.Context, reqs []state.TransactionalStateOperation) error
Close() error
}
// Interface for both sql.DB and sql.Tx
type querier interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
// sqliteDBAccess implements DBAccess.
type sqliteDBAccess struct {
logger logger.Logger
metadata sqliteMetadataStruct
db *sql.DB
ctx context.Context
cancel context.CancelFunc
// Lock only on public write API. Any public API's implementation should not call other public write APIs.
lock *sync.Mutex
}
// newSqliteDBAccess creates a new instance of sqliteDbAccess.
func newSqliteDBAccess(logger logger.Logger) *sqliteDBAccess {
return &sqliteDBAccess{
logger: logger,
lock: &sync.Mutex{},
}
}
// Init sets up SQLite Database connection and ensures that the state table
// exists.
func (a *sqliteDBAccess) Init(md state.Metadata) error {
err := a.metadata.InitWithMetadata(md)
if err != nil {
return err
}
db, err := sql.Open("sqlite", a.metadata.ConnectionString)
if err != nil {
a.logger.Error(err)
return err
}
a.db = db
a.ctx, a.cancel = context.WithCancel(context.Background())
err = a.Ping(a.ctx)
if err != nil {
return err
}
err = a.ensureStateTable(a.ctx, a.metadata.TableName)
if err != nil {
return err
}
a.scheduleCleanupExpiredData()
return nil
}
func (a *sqliteDBAccess) Ping(parentCtx context.Context) error {
a.lock.Lock()
defer a.lock.Unlock()
ctx, cancel := context.WithTimeout(parentCtx, a.metadata.timeout)
err := a.db.PingContext(ctx)
cancel()
return err
}
func (a *sqliteDBAccess) Get(parentCtx context.Context, req *state.GetRequest) (*state.GetResponse, error) {
a.lock.Lock()
defer a.lock.Unlock()
if req.Key == "" {
return nil, errors.New("missing key in get operation")
}
var (
value []byte
isBinary bool
etag string
)
// Sprintf is required for table name because sql.DB does not substitute parameters for table names
//nolint:gosec
stmt := fmt.Sprintf(
`SELECT value, is_binary, etag FROM %s
WHERE
key = ?
AND (expiration_time IS NULL OR expiration_time > CURRENT_TIMESTAMP)`,
a.metadata.TableName)
ctx, cancel := context.WithTimeout(parentCtx, a.metadata.timeout)
err := a.db.QueryRowContext(ctx, stmt, req.Key).
Scan(&value, &isBinary, &etag)
cancel()
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return &state.GetResponse{}, nil
}
return nil, err
}
if isBinary {
var n int
data := make([]byte, len(value))
n, err = base64.StdEncoding.Decode(data, value)
if err != nil {
return nil, err
}
return &state.GetResponse{
Data: data[:n],
ETag: &etag,
Metadata: req.Metadata,
}, nil
}
return &state.GetResponse{
Data: value,
ETag: &etag,
Metadata: req.Metadata,
}, nil
}
func (a *sqliteDBAccess) Set(ctx context.Context, req *state.SetRequest) error {
a.lock.Lock()
defer a.lock.Unlock()
return a.doSet(ctx, a.db, req)
}
func (a *sqliteDBAccess) doSet(parentCtx context.Context, db querier, req *state.SetRequest) error {
err := state.CheckRequestOptions(req.Options)
if err != nil {
return err
}
if req.Key == "" {
return errors.New("missing key in set option")
}
if v, ok := req.Value.(string); ok && v == "" {
return fmt.Errorf("empty string is not allowed in set operation")
}
// TTL
var ttlSeconds int
ttl, ttlerr := stateutils.ParseTTL(req.Metadata)
if ttlerr != nil {
return fmt.Errorf("error parsing TTL: %w", ttlerr)
}
if ttl != nil {
ttlSeconds = *ttl
}
// Encode the value
var requestValue string
byteArray, isBinary := req.Value.([]uint8)
if isBinary {
requestValue = base64.StdEncoding.EncodeToString(byteArray)
} else {
var bt []byte
bt, err = json.Marshal(req.Value)
if err != nil {
return err
}
requestValue = string(bt)
}
// New ETag
etagObj, err := uuid.NewRandom()
if err != nil {
return err
}
newEtag := etagObj.String()
// Also resets expiration time in case of an update
expiration := "NULL"
if ttlSeconds > 0 {
expiration = fmt.Sprintf("DATETIME(CURRENT_TIMESTAMP, '+%d seconds')", ttlSeconds)
}
// Only check for etag if FirstWrite specified (ref oracledatabaseaccess)
var res sql.Result
// Sprintf is required for table name because sql.DB does not substitute parameters for table names.
// And the same is for DATETIME function's seconds parameter (which is from an integer anyways).
if req.ETag == nil || *req.ETag == "" {
var op string
if req.Options.Concurrency == state.FirstWrite {
op = "INSERT"
} else {
op = "INSERT OR REPLACE"
}
stmt := fmt.Sprintf(
`%s INTO %s
(key, value, is_binary, etag, update_time, expiration_time)
VALUES(?, ?, ?, ?, CURRENT_TIMESTAMP, %s)`,
op, a.metadata.TableName, expiration,
)
ctx, cancel := context.WithTimeout(context.Background(), a.metadata.timeout)
res, err = db.ExecContext(ctx, stmt, req.Key, requestValue, isBinary, newEtag, req.Key)
cancel()
} else {
stmt := fmt.Sprintf(
`UPDATE %s SET
value = ?,
etag = ?,
is_binary = ?,
update_time = CURRENT_TIMESTAMP,
expiration_time = %s
WHERE
key = ?
AND eTag = ?`,
a.metadata.TableName, expiration,
)
ctx, cancel := context.WithTimeout(context.Background(), a.metadata.timeout)
res, err = db.ExecContext(ctx, stmt, requestValue, newEtag, isBinary, req.Key, *req.ETag)
cancel()
}
if err != nil {
return err
}
rows, err := res.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
if req.ETag != nil && *req.ETag != "" {
return state.NewETagError(state.ETagMismatch, nil)
}
return errors.New("no item was updated")
}
return nil
}
func (a *sqliteDBAccess) Delete(ctx context.Context, req *state.DeleteRequest) error {
a.lock.Lock()
defer a.lock.Unlock()
return a.doDelete(ctx, a.db, req)
}
func (a *sqliteDBAccess) ExecuteMulti(parentCtx context.Context, reqs []state.TransactionalStateOperation) error {
a.lock.Lock()
defer a.lock.Unlock()
tx, err := a.db.BeginTx(parentCtx, nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, req := range reqs {
switch req.Operation {
case state.Upsert:
if setReq, ok := req.Request.(state.SetRequest); ok {
err = a.doSet(parentCtx, tx, &setReq)
if err != nil {
return err
}
} else {
return fmt.Errorf("expecting set request")
}
case state.Delete:
if delReq, ok := req.Request.(state.DeleteRequest); ok {
err = a.doDelete(parentCtx, tx, &delReq)
if err != nil {
return err
}
} else {
return fmt.Errorf("expecting delete request")
}
default:
// Do nothing
}
}
return tx.Commit()
}
// Close implements io.Close.
func (a *sqliteDBAccess) Close() error {
if a.cancel != nil {
a.cancel()
}
if a.db != nil {
_ = a.db.Close()
}
return nil
}
// Create table if not exists.
func (a *sqliteDBAccess) ensureStateTable(parentCtx context.Context, stateTableName string) error {
exists, err := a.tableExists(parentCtx)
if err != nil || exists {
return err
}
a.logger.Infof("Creating SQLite state table '%s'", stateTableName)
ctx, cancel := context.WithTimeout(parentCtx, a.metadata.timeout)
defer cancel()
tx, err := a.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt := fmt.Sprintf(
`CREATE TABLE %s (
key TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL,
is_binary BOOLEAN NOT NULL,
etag TEXT NOT NULL,
expiration_time TIMESTAMP DEFAULT NULL,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
stateTableName,
)
_, err = tx.Exec(stmt)
if err != nil {
return err
}
stmt = fmt.Sprintf(`CREATE INDEX idx_%s_expiration_time ON %s (expiration_time)`, stateTableName, stateTableName)
_, err = tx.Exec(stmt)
if err != nil {
return err
}
return tx.Commit()
}
// Check if table exists.
func (a *sqliteDBAccess) tableExists(parentCtx context.Context) (bool, error) {
ctx, cancel := context.WithTimeout(parentCtx, a.metadata.timeout)
defer cancel()
var exists string
// Returns 1 or 0 as a string if the table exists or not.
const q = `SELECT EXISTS (
SELECT name FROM sqlite_master WHERE type='table' AND name = ?
) AS 'exists'`
err := a.db.QueryRowContext(ctx, q, a.metadata.TableName).Scan(&exists)
return exists == "1", err
}
func (a *sqliteDBAccess) doDelete(parentCtx context.Context, db querier, req *state.DeleteRequest) error {
err := state.CheckRequestOptions(req.Options)
if err != nil {
return err
}
if req.Key == "" {
return fmt.Errorf("missing key in delete operation")
}
var result sql.Result
if req.ETag == nil || *req.ETag == "" {
// Sprintf is required for table name because sql.DB does not substitute parameters for table names.
stmt := fmt.Sprintf("DELETE FROM %s WHERE key = ?", a.metadata.TableName)
ctx, cancel := context.WithTimeout(parentCtx, a.metadata.timeout)
result, err = db.ExecContext(ctx, stmt, req.Key)
cancel()
} else {
// Sprintf is required for table name because sql.DB does not substitute parameters for table names.
stmt := fmt.Sprintf("DELETE FROM %s WHERE key = ? AND etag = ?", a.metadata.TableName)
ctx, cancel := context.WithTimeout(parentCtx, a.metadata.timeout)
result, err = db.ExecContext(ctx, stmt, req.Key, *req.ETag)
cancel()
}
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 && req.ETag != nil && *req.ETag != "" {
return state.NewETagError(state.ETagMismatch, nil)
}
return nil
}
func (a *sqliteDBAccess) scheduleCleanupExpiredData() {
if a.metadata.cleanupInterval == nil {
return
}
d := *a.metadata.cleanupInterval
a.logger.Infof("Schedule expired data clean up every %v", d)
ticker := time.NewTicker(d)
go func() {
for {
select {
case <-ticker.C:
a.cleanupTimeout()
case <-a.ctx.Done():
return
}
}
}()
}
func (a *sqliteDBAccess) cleanupTimeout() {
a.lock.Lock()
defer a.lock.Unlock()
ctx, cancel := context.WithTimeout(a.ctx, a.metadata.timeout)
defer cancel()
tx, err := a.db.BeginTx(ctx, nil)
if err != nil {
a.logger.Errorf("Error removing expired data: failed to begin transaction: %v", err)
return
}
defer tx.Rollback()
// Sprintf is required for table name because sql.DB does not substitute parameters for table names
//nolint:gosec
stmt := fmt.Sprintf(
`DELETE FROM %s
WHERE
expiration_time IS NOT NULL
AND expiration_time < CURRENT_TIMESTAMP`,
a.metadata.TableName,
)
res, err := tx.Exec(stmt)
if err != nil {
a.logger.Errorf("Error removing expired data: failed to execute query: %v", err)
return
}
cleaned, err := res.RowsAffected()
if err != nil {
a.logger.Errorf("Error removing expired data: failed to count affected rows: %v", err)
return
}
err = tx.Commit()
if err != nil {
a.logger.Errorf("Error removing expired data: failed to commit transaction: %v", err)
return
}
a.logger.Debugf("Removed %d expired rows", cleaned)
}

View File

@ -0,0 +1,785 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/components-contrib/state"
"github.com/dapr/kit/logger"
)
const (
// Environment variable containing the connection string.
// By default, uses an in-memory database for testing.
connectionStringEnvKey = "DAPR_TEST_SQLITE_DATABASE_CONNECTSTRING"
)
func TestSqliteIntegration(t *testing.T) {
connectionString := getConnectionString()
t.Run("Test init configurations", func(t *testing.T) {
testInitConfiguration(t)
})
metadata := state.Metadata{
Base: metadata.Base{
Properties: map[string]string{
"connectionString": connectionString,
"tableName": "test_state",
},
},
}
s := NewSQLiteStateStore(logger.NewLogger("test")).(*SQLiteStore)
t.Cleanup(func() {
defer s.Close()
})
if initerror := s.Init(metadata); initerror != nil {
t.Fatal(initerror)
}
t.Run("Create table succeeds", func(t *testing.T) {
testCreateTable(t, s.dbaccess.(*sqliteDBAccess))
})
t.Run("Get Set Delete one item", func(t *testing.T) {
setGetUpdateDeleteOneItem(t, s)
})
t.Run("Get item that does not exist", func(t *testing.T) {
getItemThatDoesNotExist(t, s)
})
t.Run("Get item with no key fails", func(t *testing.T) {
getItemWithNoKey(t, s)
})
t.Run("Set item with invalid (non numeric) TTL", func(t *testing.T) {
testSetItemWithInvalidTTL(t, s)
})
t.Run("Set item with negative TTL", func(t *testing.T) {
testSetItemWithNegativeTTL(t, s)
})
t.Run("Set with TTL updates the expiration field", func(t *testing.T) {
setTTLUpdatesExpiry(t, s)
})
t.Run("Set with TTL followed by set without TTL resets the expiration field", func(t *testing.T) {
setNoTTLUpdatesExpiry(t, s)
})
t.Run("Expired item cannot be read", func(t *testing.T) {
expiredStateCannotBeRead(t, s)
})
t.Run("Unexpired item be read", func(t *testing.T) {
unexpiredStateCanBeRead(t, s)
})
t.Run("Set updates the updatedate field", func(t *testing.T) {
setUpdatesTheUpdatedateField(t, s)
})
t.Run("Set item with no key fails", func(t *testing.T) {
setItemWithNoKey(t, s)
})
t.Run("Bulk set and bulk delete", func(t *testing.T) {
testBulkSetAndBulkDelete(t, s)
})
t.Run("Update and delete with etag succeeds", func(t *testing.T) {
updateAndDeleteWithEtagSucceeds(t, s)
})
t.Run("Update with old etag fails", func(t *testing.T) {
updateWithOldEtagFails(t, s)
})
t.Run("Insert with etag fails", func(t *testing.T) {
newItemWithEtagFails(t, s)
})
t.Run("Delete with invalid etag fails when first write is enforced", func(t *testing.T) {
deleteWithInvalidEtagFails(t, s)
})
t.Run("Delete item with no key fails", func(t *testing.T) {
deleteWithNoKeyFails(t, s)
})
t.Run("Delete an item that does not exist", func(t *testing.T) {
deleteItemThatDoesNotExist(t, s)
})
t.Run("Multi with delete and set", func(t *testing.T) {
multiWithDeleteAndSet(t, s)
})
t.Run("Multi with delete only", func(t *testing.T) {
multiWithDeleteOnly(t, s)
})
t.Run("Multi with set only", func(t *testing.T) {
multiWithSetOnly(t, s)
})
t.Run("Binary data", func(t *testing.T) {
key := randomKey()
err := s.Set(context.Background(), &state.SetRequest{
Key: key,
Value: []byte("🤖"),
})
require.NoError(t, err)
res, err := s.Get(context.Background(), &state.GetRequest{
Key: key,
})
require.NoError(t, err)
require.NotNil(t, res.ETag)
assert.NotEmpty(t, res.ETag)
assert.Equal(t, "🤖", string(res.Data))
})
}
// setGetUpdateDeleteOneItem validates setting one item, getting it, and deleting it.
func setGetUpdateDeleteOneItem(t *testing.T, s *SQLiteStore) {
key := randomKey()
value := &fakeItem{Color: "yellow"}
setItem(t, s, key, value, nil)
getResponse, outputObject := getItem(t, s, key)
assert.Equal(t, value, outputObject)
newValue := &fakeItem{Color: "green"}
setItem(t, s, key, newValue, getResponse.ETag)
getResponse, outputObject = getItem(t, s, key)
assert.Equal(t, newValue, outputObject)
deleteItem(t, s, key, getResponse.ETag)
}
// testCreateTable tests the ability to create the state table.
func testCreateTable(t *testing.T, dba *sqliteDBAccess) {
tableName := "test_state_creation"
oldTableName := dba.metadata.TableName
dba.metadata.TableName = tableName
defer func() {
dba.metadata.TableName = oldTableName
}()
// Drop the table if it already exists.
exists, err := dba.tableExists(context.Background())
assert.NoError(t, err)
if exists {
dropTable(t, dba.db, tableName)
}
// Create the state table and test for its existence.
err = dba.ensureStateTable(context.Background(), tableName)
assert.NoError(t, err)
exists, err = dba.tableExists(context.Background())
assert.NoError(t, err)
assert.True(t, exists)
// Drop the state table.
dropTable(t, dba.db, tableName)
}
func dropTable(t *testing.T, db *sql.DB, tableName string) {
_, err := db.Exec("DROP TABLE " + tableName)
assert.NoError(t, err)
}
func deleteItemThatDoesNotExist(t *testing.T, s *SQLiteStore) {
// Delete the item with a key not in the store.
deleteReq := &state.DeleteRequest{
Key: randomKey(),
}
err := s.Delete(context.Background(), deleteReq)
assert.NoError(t, err)
}
func multiWithSetOnly(t *testing.T, s *SQLiteStore) {
var operations []state.TransactionalStateOperation
var setRequests []state.SetRequest
for i := 0; i < 3; i++ {
req := state.SetRequest{
Key: randomKey(),
Value: randomJSON(),
}
setRequests = append(setRequests, req)
operations = append(operations, state.TransactionalStateOperation{
Operation: state.Upsert,
Request: req,
})
}
err := s.Multi(context.Background(), &state.TransactionalStateRequest{
Operations: operations,
})
assert.NoError(t, err)
for _, set := range setRequests {
assert.True(t, storeItemExists(t, s, set.Key))
deleteItem(t, s, set.Key, nil)
}
}
func multiWithDeleteOnly(t *testing.T, s *SQLiteStore) {
var operations []state.TransactionalStateOperation
var deleteRequests []state.DeleteRequest
for i := 0; i < 3; i++ {
req := state.DeleteRequest{Key: randomKey()}
// Add the item to the database.
setItem(t, s, req.Key, randomJSON(), nil) // Add the item to the database.
// Add the item to a slice of delete requests.
deleteRequests = append(deleteRequests, req)
// Add the item to the multi transaction request.
operations = append(operations, state.TransactionalStateOperation{
Operation: state.Delete,
Request: req,
})
}
err := s.Multi(context.Background(), &state.TransactionalStateRequest{
Operations: operations,
})
assert.NoError(t, err)
for _, delete := range deleteRequests {
assert.False(t, storeItemExists(t, s, delete.Key))
}
}
func multiWithDeleteAndSet(t *testing.T, s *SQLiteStore) {
var operations []state.TransactionalStateOperation
var deleteRequests []state.DeleteRequest
for i := 0; i < 3; i++ {
req := state.DeleteRequest{Key: randomKey()}
// Add the item to the database.
setItem(t, s, req.Key, randomJSON(), nil) // Add the item to the database.
// Add the item to a slice of delete requests.
deleteRequests = append(deleteRequests, req)
// Add the item to the multi transaction request.
operations = append(operations, state.TransactionalStateOperation{
Operation: state.Delete,
Request: req,
})
}
// Create the set requests.
var setRequests []state.SetRequest
for i := 0; i < 3; i++ {
req := state.SetRequest{
Key: randomKey(),
Value: randomJSON(),
}
setRequests = append(setRequests, req)
operations = append(operations, state.TransactionalStateOperation{
Operation: state.Upsert,
Request: req,
})
}
err := s.Multi(context.Background(), &state.TransactionalStateRequest{
Operations: operations,
})
assert.NoError(t, err)
for _, delete := range deleteRequests {
assert.False(t, storeItemExists(t, s, delete.Key))
}
for _, set := range setRequests {
assert.True(t, storeItemExists(t, s, set.Key))
deleteItem(t, s, set.Key, nil)
}
}
func deleteWithInvalidEtagFails(t *testing.T, s *SQLiteStore) {
// Create new item.
key := randomKey()
value := &fakeItem{Color: "mauvebrown"}
setItem(t, s, key, value, nil)
etag := "1234"
// Delete the item with a fake etag.
deleteReq := &state.DeleteRequest{
Key: key,
ETag: &etag,
Options: state.DeleteStateOption{
Concurrency: state.FirstWrite,
},
}
err := s.Delete(context.Background(), deleteReq)
assert.NotNil(t, err, "Deleting an item with the wrong etag while enforcing FirstWrite policy should fail")
}
func deleteWithNoKeyFails(t *testing.T, s *SQLiteStore) {
deleteReq := &state.DeleteRequest{
Key: "",
}
err := s.Delete(context.Background(), deleteReq)
assert.Error(t, err)
}
// newItemWithEtagFails creates a new item and also supplies a non existent ETag and requests FirstWrite, which is invalid - expect failure.
func newItemWithEtagFails(t *testing.T, s *SQLiteStore) {
value := &fakeItem{Color: "teal"}
invalidEtag := "12345"
setReq := &state.SetRequest{
Key: randomKey(),
ETag: &invalidEtag,
Value: value,
Options: state.SetStateOption{
Concurrency: state.FirstWrite,
},
}
err := s.Set(context.Background(), setReq)
assert.Error(t, err)
}
func updateWithOldEtagFails(t *testing.T, s *SQLiteStore) {
// Create and retrieve new item.
key := randomKey()
value := &fakeItem{Color: "gray"}
setItem(t, s, key, value, nil)
getResponse, _ := getItem(t, s, key)
assert.NotNil(t, getResponse.ETag)
originalEtag := getResponse.ETag
// Change the value and get the updated etag.
newValue := &fakeItem{Color: "silver"}
setItem(t, s, key, newValue, originalEtag)
_, updatedItem := getItem(t, s, key)
assert.Equal(t, newValue, updatedItem)
getResponse, _ = getItem(t, s, key)
assert.NotNil(t, getResponse.ETag)
// Update again with the original etag - expect update failure.
newValue = &fakeItem{Color: "maroon"}
setReq := &state.SetRequest{
Key: key,
ETag: originalEtag,
Value: newValue,
Options: state.SetStateOption{
Concurrency: state.FirstWrite,
},
}
err := s.Set(context.Background(), setReq)
assert.Error(t, err)
}
func updateAndDeleteWithEtagSucceeds(t *testing.T, s *SQLiteStore) {
// Create and retrieve new item.
key := randomKey()
value := &fakeItem{Color: "hazel"}
setItem(t, s, key, value, nil)
getResponse, _ := getItem(t, s, key)
assert.NotNil(t, getResponse.ETag)
// Change the value and compare.
value.Color = "purple"
setReq := &state.SetRequest{
Key: key,
ETag: getResponse.ETag,
Value: value,
Options: state.SetStateOption{
Concurrency: state.FirstWrite,
},
}
err := s.Set(context.Background(), setReq)
assert.Nil(t, err, "Setting the item should be successful")
updateResponse, updatedItem := getItem(t, s, key)
assert.Equal(t, value, updatedItem)
// ETag should change when item is updated..
assert.NotEqual(t, getResponse.ETag, updateResponse.ETag)
// Delete.
deleteReq := &state.DeleteRequest{
Key: key,
ETag: updateResponse.ETag,
Options: state.DeleteStateOption{
Concurrency: state.FirstWrite,
},
}
err = s.Delete(context.Background(), deleteReq)
assert.Nil(t, err, "Deleting an item with the right etag while enforcing FirstWrite policy should succeed")
// Item is not in the data store.
assert.False(t, storeItemExists(t, s, key))
}
// getItemThatDoesNotExist validates the behavior of retrieving an item that does not exist.
func getItemThatDoesNotExist(t *testing.T, s *SQLiteStore) {
key := randomKey()
response, outputObject := getItem(t, s, key)
assert.Nil(t, response.Data)
assert.Equal(t, "", outputObject.Color)
}
// getItemWithNoKey validates that attempting a Get operation without providing a key will return an error.
func getItemWithNoKey(t *testing.T, s *SQLiteStore) {
getReq := &state.GetRequest{
Key: "",
}
response, getErr := s.Get(context.Background(), getReq)
assert.NotNil(t, getErr)
assert.Nil(t, response)
}
// setUpdatesTheUpdatedateField proves that the updateddate is set for an update.
func setUpdatesTheUpdatedateField(t *testing.T, s *SQLiteStore) {
key := randomKey()
value := &fakeItem{Color: "orange"}
setItem(t, s, key, value, nil)
// insertdate should have a value and updatedate should be nil.
_, updatedate := getRowData(t, s, key)
assert.NotNil(t, updatedate)
// make sure update time changes from creation.
time.Sleep(1 * time.Second)
// insertdate should not change, updatedate should have a value.
value = &fakeItem{Color: "aqua"}
setItem(t, s, key, value, nil)
_, newupdatedate := getRowData(t, s, key)
assert.NotEqual(t, updatedate.String, newupdatedate.String)
deleteItem(t, s, key, nil)
}
// setTTLUpdatesExpiry proves that the expirydate is set when a TTL is passed for a key.
func setTTLUpdatesExpiry(t *testing.T, s *SQLiteStore) {
key := randomKey()
value := &fakeItem{Color: "darkgray"}
setOptions := state.SetStateOption{}
setReq := &state.SetRequest{
Key: key,
ETag: nil,
Value: value,
Options: setOptions,
Metadata: map[string]string{
"ttlInSeconds": "1000",
},
}
err := s.Set(context.Background(), setReq)
assert.NoError(t, err)
// expirationTime should be set (to a date in the future).
_, expirationTime := getTimesForRow(t, s, key)
assert.NotNil(t, expirationTime)
assert.True(t, expirationTime.Valid, "Expiration Time should have a value after set with TTL value")
deleteItem(t, s, key, nil)
}
// setNoTTLUpdatesExpiry proves that the expirydate is reset when a state element with expiration time (TTL) loses TTL upon second set without TTL.
func setNoTTLUpdatesExpiry(t *testing.T, s *SQLiteStore) {
key := randomKey()
value := &fakeItem{Color: "darkorange"}
setOptions := state.SetStateOption{}
setReq := &state.SetRequest{
Key: key,
ETag: nil,
Value: value,
Options: setOptions,
Metadata: map[string]string{
"ttlInSeconds": "1000",
},
}
err := s.Set(context.Background(), setReq)
assert.NoError(t, err)
delete(setReq.Metadata, "ttlInSeconds")
err = s.Set(context.Background(), setReq)
assert.NoError(t, err)
// expirationTime should not be set.
_, expirationTime := getTimesForRow(t, s, key)
assert.True(t, !expirationTime.Valid, "Expiration Time should not have a value after first being set with TTL value and then being set without TTL value")
deleteItem(t, s, key, nil)
}
// expiredStateCannotBeRead proves that an expired state element can not be read.
func expiredStateCannotBeRead(t *testing.T, s *SQLiteStore) {
key := randomKey()
value := &fakeItem{Color: "darkgray"}
setOptions := state.SetStateOption{}
setReq := &state.SetRequest{
Key: key,
ETag: nil,
Value: value,
Options: setOptions,
Metadata: map[string]string{
"ttlInSeconds": "1",
},
}
err := s.Set(context.Background(), setReq)
assert.NoError(t, err)
time.Sleep(time.Second * time.Duration(2))
getResponse, err := s.Get(context.Background(), &state.GetRequest{Key: key})
assert.Equal(t, &state.GetResponse{}, getResponse, "Response must be empty")
assert.NoError(t, err, "Expired element must not be treated as error")
deleteItem(t, s, key, nil)
}
// unexpiredStateCanBeRead proves that a state element with TTL - but no yet expired - can be read.
func unexpiredStateCanBeRead(t *testing.T, s *SQLiteStore) {
key := randomKey()
value := &fakeItem{Color: "dark white"}
setOptions := state.SetStateOption{}
setReq := &state.SetRequest{
Key: key,
ETag: nil,
Value: value,
Options: setOptions,
Metadata: map[string]string{
"ttlInSeconds": "10000",
},
}
err := s.Set(context.Background(), setReq)
assert.NoError(t, err)
_, getValue := getItem(t, s, key)
assert.Equal(t, value.Color, getValue.Color, "Response must be as set")
assert.NoError(t, err, "Unexpired element with future expiration time must not be treated as error")
deleteItem(t, s, key, nil)
}
func setItemWithNoKey(t *testing.T, s *SQLiteStore) {
setReq := &state.SetRequest{
Key: "",
}
err := s.Set(context.Background(), setReq)
assert.Error(t, err)
}
func testSetItemWithInvalidTTL(t *testing.T, s *SQLiteStore) {
setReq := &state.SetRequest{
Key: randomKey(),
Value: &fakeItem{Color: "oceanblue"},
Metadata: (map[string]string{
"ttlInSeconds": "XX",
}),
}
err := s.Set(context.Background(), setReq)
assert.NotNil(t, err, "Setting a value with a proper key and a incorrect TTL value should be produce an error")
}
func testSetItemWithNegativeTTL(t *testing.T, s *SQLiteStore) {
setReq := &state.SetRequest{
Key: randomKey(),
Value: &fakeItem{Color: "oceanblue"},
Metadata: (map[string]string{
"ttlInSeconds": "-10",
}),
}
err := s.Set(context.Background(), setReq)
assert.NotNil(t, err, "Setting a value with a proper key and a negative (other than -1) TTL value should be produce an error")
}
// Tests valid bulk sets and deletes.
func testBulkSetAndBulkDelete(t *testing.T, s *SQLiteStore) {
setReq := []state.SetRequest{
{
Key: randomKey(),
Value: &fakeItem{Color: "oceanblue"},
},
{
Key: randomKey(),
Value: &fakeItem{Color: "livingwhite"},
},
}
err := s.BulkSet(context.Background(), setReq)
assert.NoError(t, err)
assert.True(t, storeItemExists(t, s, setReq[0].Key))
assert.True(t, storeItemExists(t, s, setReq[1].Key))
deleteReq := []state.DeleteRequest{
{
Key: setReq[0].Key,
},
{
Key: setReq[1].Key,
},
}
err = s.BulkDelete(context.Background(), deleteReq)
assert.NoError(t, err)
assert.False(t, storeItemExists(t, s, setReq[0].Key))
assert.False(t, storeItemExists(t, s, setReq[1].Key))
}
// testInitConfiguration tests valid and invalid config settings.
func testInitConfiguration(t *testing.T) {
logger := logger.NewLogger("test")
tests := []struct {
name string
props map[string]string
expectedErr string
}{
{
name: "Empty",
props: map[string]string{},
expectedErr: errMissingConnectionString,
},
{
name: "Valid connection string",
props: map[string]string{
"connectionString": getConnectionString(),
"tableName": "test_state",
},
expectedErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewSQLiteStateStore(logger).(*SQLiteStore)
defer p.Close()
metadata := state.Metadata{
Base: metadata.Base{
Properties: tt.props,
},
}
err := p.Init(metadata)
if tt.expectedErr == "" {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, err.Error(), tt.expectedErr)
}
})
}
}
func getConnectionString() string {
s := os.Getenv(connectionStringEnvKey)
if s == "" {
// default integration test with in-memory db.
s = ":memory:"
}
return s
}
func setItem(t *testing.T, s *SQLiteStore, key string, value interface{}, etag *string) {
setOptions := state.SetStateOption{}
if etag != nil {
setOptions.Concurrency = state.FirstWrite
}
setReq := &state.SetRequest{
Key: key,
ETag: etag,
Value: value,
Options: setOptions,
}
err := s.Set(context.Background(), setReq)
assert.NoError(t, err)
itemExists := storeItemExists(t, s, key)
assert.True(t, itemExists, "Item should exist after set has been executed ")
}
func getItem(t *testing.T, s *SQLiteStore, key string) (*state.GetResponse, *fakeItem) {
getReq := &state.GetRequest{
Key: key,
Options: state.GetStateOption{},
}
response, getErr := s.Get(context.Background(), getReq)
assert.Nil(t, getErr)
assert.NotNil(t, response)
outputObject := &fakeItem{}
_ = json.Unmarshal(response.Data, outputObject)
return response, outputObject
}
func deleteItem(t *testing.T, s *SQLiteStore, key string, etag *string) {
deleteReq := &state.DeleteRequest{
Key: key,
ETag: etag,
Options: state.DeleteStateOption{},
}
deleteErr := s.Delete(context.Background(), deleteReq)
assert.Nil(t, deleteErr)
assert.False(t, storeItemExists(t, s, key), "item should no longer exist after delete has been performed")
}
func storeItemExists(t *testing.T, s *SQLiteStore, key string) bool {
dba := s.dbaccess.(*sqliteDBAccess)
tableName := dba.metadata.TableName
db := dba.db
var rowCount int32
stmttpl := "SELECT count(key) FROM %s WHERE key = ?"
statement := fmt.Sprintf(stmttpl, tableName)
err := db.QueryRow(statement, key).Scan(&rowCount)
assert.NoError(t, err)
exists := rowCount > 0
return exists
}
func getRowData(t *testing.T, s *SQLiteStore, key string) (returnValue string, updatedate sql.NullString) {
dba := s.dbaccess.(*sqliteDBAccess)
tableName := dba.metadata.TableName
db := dba.db
err := db.QueryRow(fmt.Sprintf("SELECT value, update_time FROM %s WHERE key = ?", tableName), key).
Scan(&returnValue, &updatedate)
assert.NoError(t, err)
return returnValue, updatedate
}
func getTimesForRow(t *testing.T, s *SQLiteStore, key string) (updatedate sql.NullString, expirationtime sql.NullString) {
dba := s.dbaccess.(*sqliteDBAccess)
tableName := dba.metadata.TableName
db := dba.db
err := db.QueryRow(fmt.Sprintf("SELECT update_time, expiration_time FROM %s WHERE key = ?", tableName), key).
Scan(&updatedate, &expirationtime)
assert.NoError(t, err)
return updatedate, expirationtime
}

View File

@ -0,0 +1,117 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sqlite
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/components-contrib/state"
"github.com/dapr/kit/ptr"
)
const (
defaultTableName = "state"
defaultCleanupInternal = 3600 // In seconds = 1 hour
defaultTimeout = 20 // Default timeout for database requests, in seconds
errMissingConnectionString = "missing connection string"
errInvalidIdentifier = "invalid identifier: %s" // specify identifier type, e.g. "table name"
)
type sqliteMetadataStruct struct {
ConnectionString string `json:"connectionString" mapstructure:"connectionString"`
TableName string `json:"tableName" mapstructure:"tableName"`
TimeoutInSeconds string `json:"timeoutInSeconds" mapstructure:"timeoutInSeconds"`
CleanupIntervalInSeconds string `json:"cleanupIntervalInSeconds" mapstructure:"cleanupIntervalInSeconds"`
timeout time.Duration
cleanupInterval *time.Duration
}
func (m *sqliteMetadataStruct) InitWithMetadata(meta state.Metadata) error {
// Reset the object
m.ConnectionString = ""
m.TableName = defaultTableName
m.cleanupInterval = ptr.Of(defaultCleanupInternal * time.Second)
m.timeout = defaultTimeout * time.Second
// Decode the metadata
err := metadata.DecodeMetadata(meta.Properties, &m)
if err != nil {
return err
}
// Validate and sanitize input
if m.ConnectionString == "" {
return errors.New(errMissingConnectionString)
}
if !validIdentifier(m.TableName) {
return fmt.Errorf(errInvalidIdentifier, m.TableName)
}
// Timeout
if m.TimeoutInSeconds != "" {
timeoutInSec, err := strconv.ParseInt(m.TimeoutInSeconds, 10, 0)
if err != nil {
return fmt.Errorf("invalid value for 'timeoutInSeconds': %s", m.TimeoutInSeconds)
}
if timeoutInSec < 1 {
return errors.New("invalid value for 'timeoutInSeconds': must be greater than 0")
}
m.timeout = time.Duration(timeoutInSec) * time.Second
}
// Cleanup interval
// Nil duration means never clean up expired data
if m.CleanupIntervalInSeconds != "" {
cleanupIntervalInSec, err := strconv.ParseInt(m.CleanupIntervalInSeconds, 10, 0)
if err != nil {
return fmt.Errorf("invalid value for 'cleanupIntervalInSeconds': %s", m.CleanupIntervalInSeconds)
}
// Non-positive value from meta means disable auto cleanup.
if cleanupIntervalInSec > 0 {
m.cleanupInterval = ptr.Of(time.Duration(cleanupIntervalInSec) * time.Second)
} else {
m.cleanupInterval = nil
}
}
return nil
}
// Validates an identifier, such as table or DB name.
func validIdentifier(v string) bool {
if v == "" {
return false
}
// Loop through the string as byte slice as we only care about ASCII characters
b := []byte(v)
for i := 0; i < len(b); i++ {
if (b[i] >= '0' && b[i] <= '9') ||
(b[i] >= 'a' && b[i] <= 'z') ||
(b[i] >= 'A' && b[i] <= 'Z') ||
b[i] == '_' {
continue
}
return false
}
return true
}

189
state/sqlite/sqlite_test.go Normal file
View File

@ -0,0 +1,189 @@
/*
Copyright 2022 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sqlite
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/components-contrib/state"
"github.com/dapr/kit/logger"
)
const (
fakeConnectionString = "not a real connection"
)
// Fake implementation of interface oracledatabase.dbaccess.
type fakeDBaccess struct {
logger logger.Logger
pingExecuted bool
initExecuted bool
setExecuted bool
getExecuted bool
}
func (m *fakeDBaccess) Ping(ctx context.Context) error {
m.pingExecuted = true
return nil
}
func (m *fakeDBaccess) Init(metadata state.Metadata) error {
m.initExecuted = true
return nil
}
func (m *fakeDBaccess) Set(ctx context.Context, req *state.SetRequest) error {
m.setExecuted = true
return nil
}
func (m *fakeDBaccess) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) {
m.getExecuted = true
return nil, nil
}
func (m *fakeDBaccess) Delete(ctx context.Context, req *state.DeleteRequest) error {
return nil
}
func (m *fakeDBaccess) ExecuteMulti(ctx context.Context, reqs []state.TransactionalStateOperation) error {
return nil
}
func (m *fakeDBaccess) Close() error {
return nil
}
// Proves that the Init method runs the init method.
func TestInitRunsDBAccessInit(t *testing.T) {
t.Parallel()
ods, fake := createSqliteWithFake(t)
ods.Ping()
assert.True(t, fake.initExecuted)
}
func TestMultiWithNoRequestsReturnsNil(t *testing.T) {
t.Parallel()
var operations []state.TransactionalStateOperation
ods := createSqlite(t)
err := ods.Multi(context.Background(), &state.TransactionalStateRequest{
Operations: operations,
})
assert.NoError(t, err)
}
func TestValidSetRequest(t *testing.T) {
t.Parallel()
var operations []state.TransactionalStateOperation
operations = append(operations, state.TransactionalStateOperation{
Operation: state.Upsert,
Request: createSetRequest(),
})
ods := createSqlite(t)
err := ods.Multi(context.Background(), &state.TransactionalStateRequest{
Operations: operations,
})
assert.NoError(t, err)
}
func TestValidMultiDeleteRequest(t *testing.T) {
t.Parallel()
var operations []state.TransactionalStateOperation
operations = append(operations, state.TransactionalStateOperation{
Operation: state.Delete,
Request: createDeleteRequest(),
})
ods := createSqlite(t)
err := ods.Multi(context.Background(), &state.TransactionalStateRequest{
Operations: operations,
})
assert.NoError(t, err)
}
func createSetRequest() state.SetRequest {
return state.SetRequest{
Key: randomKey(),
Value: randomJSON(),
}
}
func createDeleteRequest() state.DeleteRequest {
return state.DeleteRequest{
Key: randomKey(),
}
}
func createSqliteWithFake(t *testing.T) (*SQLiteStore, *fakeDBaccess) {
ods := createSqlite(t)
fake := ods.dbaccess.(*fakeDBaccess)
return ods, fake
}
// Proves that the Ping method runs the ping method.
func TestPingRunsDBAccessPing(t *testing.T) {
t.Parallel()
odb, fake := createSqliteWithFake(t)
odb.Ping()
assert.True(t, fake.pingExecuted)
}
func createSqlite(t *testing.T) *SQLiteStore {
logger := logger.NewLogger("test")
dba := &fakeDBaccess{
logger: logger,
}
odb := newSQLiteStateStore(logger, dba)
assert.NotNil(t, odb)
metadata := &state.Metadata{
Base: metadata.Base{
Properties: map[string]string{
"connectionString": fakeConnectionString,
},
},
}
err := odb.Init(*metadata)
assert.NoError(t, err)
assert.NotNil(t, odb.dbaccess)
return odb
}
func randomKey() string {
return uuid.New().String()
}
type fakeItem struct {
Color string
}
func randomJSON() *fakeItem {
return &fakeItem{Color: randomKey()}
}

View File

@ -0,0 +1,10 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.sqlite
metadata:
# For these tests, use an in-memory database
- name: connectionString
value: ":memory:"

View File

@ -24,6 +24,8 @@ components:
operations: [ "set", "get", "delete", "bulkset", "bulkdelete", "transaction", "etag", "first-write" ]
- component: postgresql
allOperations: true
- component: sqlite
operations: [ "set", "get", "delete", "bulkset", "bulkdelete", "transaction", "etag", "first-write", "ttl" ]
- component: mysql.mysql
allOperations: false
operations: [ "set", "get", "delete", "bulkset", "bulkdelete", "transaction", "etag", "first-write" ]

View File

@ -92,6 +92,7 @@ import (
s_postgresql "github.com/dapr/components-contrib/state/postgresql"
s_redis "github.com/dapr/components-contrib/state/redis"
s_rethinkdb "github.com/dapr/components-contrib/state/rethinkdb"
s_sqlite "github.com/dapr/components-contrib/state/sqlite"
s_sqlserver "github.com/dapr/components-contrib/state/sqlserver"
conf_bindings "github.com/dapr/components-contrib/tests/conformance/bindings"
conf_configuration "github.com/dapr/components-contrib/tests/conformance/configuration"
@ -541,6 +542,8 @@ func loadStateStore(tc TestComponent) state.Store {
store = s_sqlserver.NewSQLServerStateStore(testLogger)
case "postgresql":
store = s_postgresql.NewPostgreSQLStateStore(testLogger)
case "sqlite":
store = s_sqlite.NewSQLiteStateStore(testLogger)
case "mysql.mysql":
store = s_mysql.NewMySQLStateStore(testLogger)
case "mysql.mariadb":