New SQLite 3 state store
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
parent
570f67b797
commit
6f71753c13
|
@ -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
16
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()}
|
||||
}
|
|
@ -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:"
|
|
@ -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" ]
|
||||
|
|
|
@ -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":
|
||||
|
|
Loading…
Reference in New Issue