pax_global_header00006660000000000000000000000064140553771320014521gustar00rootroot0000000000000052 comment=78efc06eb396a438ff13c00f0d2015dacc136b23 simplekv-1.1.0/000077500000000000000000000000001405537713200133525ustar00rootroot00000000000000simplekv-1.1.0/.travis.yml000066400000000000000000000002171405537713200154630ustar00rootroot00000000000000language: go go: - "1.15" - "1.16" script: GO111MODULE=on go test ./... services: - mongodb - postgresql addons: postgresql: "9.5" simplekv-1.1.0/LICENSE000066400000000000000000000215011405537713200143560ustar00rootroot00000000000000All files in this repository are licensed as follows. If you contribute to this repository, it is assumed that you license your contribution under the same license unless you state otherwise. All files Copyright (C) 2018 Canonical Ltd. unless otherwise specified in the file. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. simplekv-1.1.0/README.md000066400000000000000000000002531405537713200146310ustar00rootroot00000000000000simplekv: a simple key-value store with multiple backends This repository provides a naive key-value store with Postgres, MongoDB and in-memory backend implementations. simplekv-1.1.0/go.mod000066400000000000000000000014111405537713200144550ustar00rootroot00000000000000module github.com/juju/simplekv go 1.12 require ( github.com/frankban/quicktest v1.1.1 github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c // indirect github.com/juju/errors v0.0.0-20190207033735-e65537c515d7 // indirect github.com/juju/loggo v0.0.0-20190212223446-d976af380377 // indirect github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 github.com/juju/mgotest v1.0.2 github.com/juju/postgrestest v1.1.0 github.com/juju/retry v0.0.0-20180821225755-9058e192b216 // indirect github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 // indirect github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d github.com/lib/pq v1.0.0 golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect gopkg.in/errgo.v1 v1.0.0 gopkg.in/retry.v1 v1.0.2 ) simplekv-1.1.0/go.sum000066400000000000000000000141521405537713200145100ustar00rootroot00000000000000github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.1.1 h1:X1oR7hhKU/Y12Ym8JWNrOteniQR+ws29P21czLD72kw= github.com/frankban/quicktest v1.1.1/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/errors v0.0.0-20180726005433-812b06ada177/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20190207033735-e65537c515d7 h1:dMIPRDg6gi7CUp0Kj2+HxqJ5kTr1iAdzsXYIrLCNSmU= github.com/juju/errors v0.0.0-20190207033735-e65537c515d7/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190212223446-d976af380377 h1:n6QjW3g5JNY3xPmIjFt6z1H6tFQA6BhwOC2bvTAm1YU= github.com/juju/loggo v0.0.0-20190212223446-d976af380377/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 h1:/WiCm+Vpj87e4QWuWwPD/bNE9kDrWCLvPBHOQNcG2+A= github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= github.com/juju/mgotest v1.0.2 h1:rgeY0zbfWvxsuCz9m13VAGPFQVzQJeSZOnJ/AzkrkRQ= github.com/juju/mgotest v1.0.2/go.mod h1:04v1Xi2RiTO3h77YWtaXB2LAaGRSSi+Vl4hOV1coD0k= github.com/juju/postgrestest v1.1.0 h1:jEGPSV72rQuQGSzBZfEU15As6FqThPWgNz8ycKD1d1E= github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= github.com/juju/retry v0.0.0-20160928201858-1998d01ba1c3/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= github.com/juju/testing v0.0.0-20180517134105-72703b1e95eb/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 h1:WQM1NildKThwdP7qWrNAFGzp4ijNLw8RlgENkaI4MJs= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/juju/utils v0.0.0-20180619112806-c746c6e86f4f/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY= github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v1 v1.0.0 h1:n+7XfCyygBFb8sEjg6692xjC6Us50TFRO54+xYUEwjE= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/retry.v1 v1.0.2 h1:PxdjsMtWk8Yk224+P2Umsbc7D6niLXw2VNcl2tr/fVY= gopkg.in/retry.v1 v1.0.2/go.mod h1:tLRIBNXxoKtalyAWBSIbHdWkIBN2x9jVEm5l0Z+BjXs= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= simplekv-1.1.0/internal/000077500000000000000000000000001405537713200151665ustar00rootroot00000000000000simplekv-1.1.0/internal/simplekvtest/000077500000000000000000000000001405537713200177205ustar00rootroot00000000000000simplekv-1.1.0/internal/simplekvtest/keyvalue.go000066400000000000000000000172371405537713200221060ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package simplekvtest import ( "context" "fmt" "reflect" "strings" "testing" "time" qt "github.com/frankban/quicktest" errgo "gopkg.in/errgo.v1" "github.com/juju/simplekv" "github.com/juju/utils" ) // TestStore runs a set of tests to check that a given // store implementation works correctly. The newStore // function will be called to create new store instances // for the tests - each one should be independent // of the others. func TestStore(t *testing.T, newStore func() (_ simplekv.Store, err error)) { runTests(qt.New(t), &suite{ newStore: newStore, }) } type suite struct { newStore func() (_ simplekv.Store, err error) ctx context.Context closeContext func() kv simplekv.Store } func (s *suite) SetUpTest(c *qt.C) { var err error s.kv, err = s.newStore() c.Assert(err, qt.Equals, nil) s.ctx, s.closeContext = s.kv.Context(context.Background()) } func (s *suite) TearDownTest(c *qt.C) { s.closeContext() } func (s *suite) TestSet(c *qt.C) { ctx := s.ctx err := s.kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.Equals, nil) result, err := s.kv.Get(ctx, "test-key") c.Assert(err, qt.Equals, nil) c.Assert(string(result), qt.Equals, "test-value") // Try again with an existing record, which might trigger different behavior. err = s.kv.Set(ctx, "test-key", []byte("test-value-2"), time.Time{}) c.Assert(err, qt.Equals, nil) result, err = s.kv.Get(ctx, "test-key") c.Assert(err, qt.Equals, nil) c.Assert(string(result), qt.Equals, "test-value-2") } func (s *suite) TestGetNotFound(c *qt.C) { ctx := s.ctx _, err := s.kv.Get(ctx, "test-not-there-key") c.Assert(errgo.Cause(err), qt.Equals, simplekv.ErrNotFound) c.Assert(err, qt.ErrorMatches, "key test-not-there-key not found") } func (s *suite) TestSetKeyOnce(c *qt.C) { ctx := s.ctx err := simplekv.SetKeyOnce(ctx, s.kv, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.Equals, nil) result, err := s.kv.Get(ctx, "test-key") c.Assert(err, qt.Equals, nil) c.Assert(string(result), qt.Equals, "test-value") } func (s *suite) TestSetKeyOnceDuplicate(c *qt.C) { ctx := s.ctx err := simplekv.SetKeyOnce(ctx, s.kv, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.Equals, nil) err = simplekv.SetKeyOnce(ctx, s.kv, "test-key", []byte("test-value"), time.Time{}) c.Assert(errgo.Cause(err), qt.Equals, simplekv.ErrDuplicateKey) c.Assert(err, qt.ErrorMatches, "key test-key already exists") } func (s *suite) TestUpdateSuccessWithPreexistingKey(c *qt.C) { ctx := s.ctx err := s.kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.Equals, nil) err = s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(string(oldVal), qt.Equals, "test-value") return []byte("test-value-2"), nil }) c.Assert(err, qt.Equals, nil) val, err := s.kv.Get(ctx, "test-key") c.Assert(err, qt.Equals, nil) c.Assert(string(val), qt.Equals, "test-value-2") } func (s *suite) TestUpdateSuccessWithoutPreexistingKey(c *qt.C) { ctx := s.ctx err := s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(oldVal, qt.IsNil) return []byte("test-value"), nil }) c.Assert(err, qt.Equals, nil) val, err := s.kv.Get(ctx, "test-key") c.Assert(err, qt.Equals, nil) c.Assert(string(val), qt.Equals, "test-value") } func (s *suite) TestUpdateConcurrent(c *qt.C) { ctx := s.ctx const N = 100 done := make(chan struct{}) for i := 0; i < 2; i++ { go func() { for j := 0; j < N; j++ { err := s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { time.Sleep(time.Millisecond) if oldVal == nil { return []byte{1}, nil } return []byte{oldVal[0] + 1}, nil }) c.Check(err, qt.Equals, nil) } done <- struct{}{} }() } <-done <-done val, err := s.kv.Get(ctx, "test-key") c.Assert(err, qt.Equals, nil) c.Assert(val, qt.HasLen, 1) c.Assert(int(val[0]), qt.Equals, N*2) } func (s *suite) TestUpdateErrorWithExistingKey(c *qt.C) { ctx := s.ctx testErr := errgo.Newf("test error") err := s.kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.Equals, nil) err = s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(string(oldVal), qt.Equals, "test-value") return nil, testErr }) c.Check(errgo.Cause(err), qt.Equals, testErr) } func (s *suite) TestUpdateErrorWithNonExistentKey(c *qt.C) { ctx := s.ctx testErr := errgo.Newf("test error") err := s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(oldVal, qt.IsNil) return nil, testErr }) c.Check(errgo.Cause(err), qt.Equals, testErr) } func (s *suite) TestSetNilUpdatesAsNonNil(c *qt.C) { ctx := s.ctx err := s.kv.Set(ctx, "test-key", nil, time.Time{}) c.Assert(err, qt.Equals, nil) err = s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Assert(oldVal, qt.DeepEquals, []byte{}) return nil, nil }) c.Assert(err, qt.Equals, nil) } func (s *suite) TestUpdateReturnNilThenUpdatesAsNonNil(c *qt.C) { ctx := s.ctx err := s.kv.Set(ctx, "test-key", []byte("test-value"), time.Time{}) c.Assert(err, qt.Equals, nil) err = s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(string(oldVal), qt.Equals, "test-value") return nil, nil }) c.Assert(err, qt.Equals, nil) err = s.kv.Update(ctx, "test-key", time.Time{}, func(oldVal []byte) ([]byte, error) { c.Check(oldVal, qt.Not(qt.IsNil)) c.Assert(oldVal, qt.DeepEquals, []byte{}) return nil, nil }) c.Assert(err, qt.Equals, nil) } func (s *suite) TestKeys(c *qt.C) { ctx := s.ctx kv, ok := s.kv.(simplekv.KeyLister) c.Assert(ok, qt.Equals, true) keys, err := kv.Keys(ctx) c.Assert(err, qt.Equals, nil) c.Assert(keys, qt.HasLen, 0) N := 100 expected := make(map[string]bool) for i := 0; i < N; i++ { key := utils.MustNewUUID().String() value := fmt.Sprintf("value-%d", i) err := s.kv.Set(ctx, key, []byte(value), time.Time{}) c.Assert(err, qt.Equals, nil) expected[key] = true } keys, err = kv.Keys(ctx) c.Assert(err, qt.Equals, nil) c.Assert(keys, qt.HasLen, len(expected)) for _, key := range keys { _, ok := expected[key] c.Assert(ok, qt.Equals, true) } } // TODO factor the runTests function into a separate public repo somewhere. // runTests runs all methods on the given value that have the // prefix "Test". The signature of the test methods must be // func(*quicktest.C). // // If s is is a pointer, the value pointed to is copied // before any methods are invoked on it; a new copy // is made for each test. // // If there is a method named SetUpTest, it will be // invoked before each test method runs. // // If there is a method named TearDownTest, it will // be invoked after each test method runs. // // If present the signature of both SetUpTest and TearDownTest // must be func(*quicktest.C). func runTests(c *qt.C, s interface{}) { sv := reflect.ValueOf(s) st := sv.Type() for i := 0; i < st.NumMethod(); i++ { sv := sv if st.Kind() == reflect.Ptr { // Make a copy (this makes it possible to have // parallel tests). sv1 := reflect.New(st.Elem()) sv1.Elem().Set(sv.Elem()) sv = sv1 } methodName := st.Method(i).Name name := strings.TrimPrefix(methodName, "Test") if len(name) == len(methodName) { continue } c.Run(name, func(c *qt.C) { args := []reflect.Value{reflect.ValueOf(c)} if setUp := sv.MethodByName("SetUpTest"); setUp.IsValid() { setUp.Call(args) } if tearDown := sv.MethodByName("TearDownTest"); tearDown.IsValid() { defer tearDown.Call(args) } sv.Method(i).Call(args) }) } } simplekv-1.1.0/kv.go000066400000000000000000000062551405537713200143310ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package simplekv import ( "context" "time" errgo "gopkg.in/errgo.v1" ) var ( // ErrNotFound is the error cause used when an identity cannot be // found in storage. ErrNotFound = errgo.New("not found") // ErrDuplicateKey is the error cause used when SetKeyOnce // tries to set a duplicate key. ErrDuplicateKey = errgo.New("duplicate key") ) // KeyNotFoundError creates a new error with a cause of ErrNotFound and // an appropriate message. func KeyNotFoundError(key string) error { err := errgo.WithCausef(nil, ErrNotFound, "key %s not found", key) err.(*errgo.Err).SetLocation(1) return err } // Store holds the interface implemented by the various backend implementations. type Store interface { // Context returns a context that is suitable for passing to the // other Store methods. Store methods called with // such a context will be sequentially consistent; for example, a // value that is set in Set will immediately be available from // Get. // // The returned close function must be called when the returned // context will no longer be used, to allow for any required // cleanup. Context(ctx context.Context) (_ context.Context, close func()) // Get retrieves the value associated with the given key. If // there is no such key an error with a cause of ErrNotFound will // be returned. Get(ctx context.Context, key string) ([]byte, error) // Set updates the given key to have the specified value. // // If the expire time is non-zero then the entry may be garbage // collected at some point after that time. Clients should not // rely on the value being removed at the given time. Set(ctx context.Context, key string, value []byte, expire time.Time) error // Update updates the value for the given key. The getVal // function is called with the old value of the key and should // return the new value, which will be updated atomically; // getVal may be called several times, so should not have // side-effects. // // If an entry for the given key did not previously exist, old // will be nil. // // If getVal returns an error, it will be returned by Update with // its cause unchanged. // // If the expire time is non-zero then the entry may be garbage // collected at some point after that time. Clients should not // rely on the value being removed at the given time. Update(ctx context.Context, key string, expire time.Time, getVal func(old []byte) ([]byte, error)) error } // KeyLister holds the interface used to list keys store in the Store. type KeyLister interface { Store // Keys returns a distinct list of stored keys. Keys(ctx context.Context) ([]string, error) } // SetKeyOnce is like Store.Set except that if the key already // has a value associated with it it returns an error with a cause of // ErrDuplicateKey. func SetKeyOnce(ctx context.Context, kv Store, key string, value []byte, expire time.Time) error { err := kv.Update(ctx, key, expire, func(old []byte) ([]byte, error) { if old != nil { return nil, errgo.WithCausef(nil, ErrDuplicateKey, "key %s already exists", key) } return value, nil }) return errgo.Mask(err, errgo.Is(ErrDuplicateKey)) } simplekv-1.1.0/memsimplekv/000077500000000000000000000000001405537713200157035ustar00rootroot00000000000000simplekv-1.1.0/memsimplekv/keyvalue.go000066400000000000000000000033121405537713200200560ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package memsimplekv import ( "context" "sync" "time" errgo "gopkg.in/errgo.v1" "github.com/juju/simplekv" ) // NewStore returns a new Store instance. func NewStore() simplekv.Store { return &kvStore{ data: make(map[string][]byte), } } type kvStore struct { mu sync.Mutex data map[string][]byte } // Context implements simplekv.Store.Context by returning the given // context unchanged and a nop close function. func (s *kvStore) Context(ctx context.Context) (_ context.Context, close func()) { return ctx, func() {} } // Get implements simplekv.Store.Get. func (s *kvStore) Get(_ context.Context, key string) ([]byte, error) { s.mu.Lock() defer s.mu.Unlock() v, ok := s.data[key] if !ok { return nil, simplekv.KeyNotFoundError(key) } return v, nil } // Set implements simplekv.Store.Set. func (s *kvStore) Set(_ context.Context, key string, value []byte, _ time.Time) error { s.mu.Lock() defer s.mu.Unlock() if value == nil { value = []byte{} } s.data[key] = value return nil } // Update implements simplekv.Store.Update. func (s *kvStore) Update(ctx context.Context, key string, expire time.Time, getVal func(old []byte) ([]byte, error)) error { s.mu.Lock() defer s.mu.Unlock() newVal, err := getVal(s.data[key]) if err != nil { return errgo.Mask(err, errgo.Any) } if newVal == nil { newVal = []byte{} } s.data[key] = newVal return nil } // Keys implements simplekv.Store.Keys. func (s *kvStore) Keys(_ context.Context) ([]string, error) { s.mu.Lock() defer s.mu.Unlock() keys := make([]string, len(s.data)) i := 0 for k, _ := range s.data { keys[i] = k i++ } return keys, nil } simplekv-1.1.0/memsimplekv/keyvalue_test.go000066400000000000000000000006171405537713200211220ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package memsimplekv_test import ( "testing" "github.com/juju/simplekv" "github.com/juju/simplekv/internal/simplekvtest" "github.com/juju/simplekv/memsimplekv" ) func TestMemStore(t *testing.T) { simplekvtest.TestStore(t, func() (simplekv.Store, error) { return memsimplekv.NewStore(), nil }) } simplekv-1.1.0/mgosimplekv/000077500000000000000000000000001405537713200157075ustar00rootroot00000000000000simplekv-1.1.0/mgosimplekv/kv.go000066400000000000000000000121451405537713200166610ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package mgosimplekv import ( "bytes" "context" "time" mgo "github.com/juju/mgo/v2" "github.com/juju/mgo/v2/bson" errgo "gopkg.in/errgo.v1" retry "gopkg.in/retry.v1" "github.com/juju/simplekv" ) type sessionKey struct{} // kvStore implements simplekv.Store. type kvStore struct { coll *mgo.Collection } // NewStore returns a new Store implementation that uses // the given mongo collection for storage. func NewStore(coll *mgo.Collection) (simplekv.Store, error) { if err := coll.EnsureIndex(mgo.Index{ Key: []string{"expire"}, ExpireAfter: time.Second, }); err != nil { return nil, errgo.Mask(err) } return &kvStore{ coll: coll, }, nil } // Context implements simplekv.Context by copying the kvStore's underlying // session if one isn't already present in the context. func (s *kvStore) Context(ctx context.Context) (_ context.Context, close func()) { if session, _ := ctx.Value(sessionKey{}).(*mgo.Session); session != nil { return ctx, func() {} } // TODO provide some way for the caller to associate their own // session with the context so that they can implement session // pooling if desired? session := s.coll.Database.Session.Copy() return ContextWithSession(ctx, session), session.Close } // session returns a *mgo.Session for use in subsequent queries. The returned // session must be closed once finished with. func (s *kvStore) session(ctx context.Context) *mgo.Session { if s, _ := ctx.Value(sessionKey{}).(*mgo.Session); s != nil { return s.Clone() } return s.coll.Database.Session.Copy() } // c returns the store's collection associated with any session found in the // context. The collection's underlying session must be closed when the // query is complete. func (s *kvStore) c(ctx context.Context) *mgo.Collection { return s.coll.With(s.session(ctx)) } type kvDoc struct { Key string `bson:"_id"` Value []byte `bson:"value'` Expire time.Time `bson:",omitempty"` } // Get implements simplekv.Store.Get by retrieving the document with // the given key from the store's collection. func (s *kvStore) Get(ctx context.Context, key string) ([]byte, error) { coll := s.c(ctx) defer coll.Database.Session.Close() var doc kvDoc if err := coll.FindId(key).One(&doc); err != nil { if errgo.Cause(err) == mgo.ErrNotFound { return nil, simplekv.KeyNotFoundError(key) } return nil, errgo.Mask(err) } return doc.Value, nil } // Set implements simplekv.Store.Set by upserting the document with // the given key, value and expire time into the store's collection. func (s *kvStore) Set(ctx context.Context, key string, value []byte, expire time.Time) error { coll := s.c(ctx) defer coll.Database.Session.Close() _, err := coll.UpsertId(key, bson.D{{ "$set", bson.D{{ "value", value, }, { "expire", expire, }}, }}) return errgo.Mask(err) } var updateStrategy = retry.Exponential{ Initial: time.Microsecond, Factor: 2, MaxDelay: 500 * time.Millisecond, Jitter: true, } // Update implements simplekv.Store.Update. func (s *kvStore) Update(ctx context.Context, key string, expire time.Time, getVal func(old []byte) ([]byte, error)) error { coll := s.c(ctx) defer coll.Database.Session.Close() r := retry.StartWithCancel(updateStrategy, nil, ctx.Done()) for r.Next() { var doc kvDoc if err := coll.Find(bson.D{{"_id", key}}).One(&doc); err != nil { if errgo.Cause(err) != mgo.ErrNotFound { return errgo.Mask(err) } newVal, err := getVal(nil) if err != nil { return errgo.Mask(err, errgo.Any) } err = coll.Insert(kvDoc{ Key: key, Value: newVal, Expire: expire, }) if err == nil { return nil } if !mgo.IsDup(err) { return errgo.Mask(err) } // A new document has been inserted after we did the FindId and before Insert, // so try again. continue } newVal, err := getVal(doc.Value) if err != nil { return errgo.Mask(err, errgo.Any) } if bytes.Equal(newVal, doc.Value) { return nil } err = coll.Update(bson.D{{ "_id", key, }, { "value", doc.Value, }}, bson.D{{ "$set", bson.D{{ "value", newVal, }, { "expire", expire, }}, }}) if err == nil { return nil } if err != mgo.ErrNotFound { return errgo.Mask(err) } // The document has been removed or updated since we retrieved it, // so try again. } if r.Stopped() { return errgo.Notef(ctx.Err(), "cannot update key") } return errgo.Newf("too many retry attempts trying to update key") } // Keys implements simplekv.Store.Keys. func (s *kvStore) Keys(ctx context.Context) ([]string, error) { coll := s.c(ctx) defer coll.Database.Session.Close() var keys []string if err := coll.Find(bson.M{}).Distinct("_id", &keys); err != nil { return nil, errgo.Mask(err) } return keys, nil } // ContextWithSession returns the given context associated with the given // session. When the context is passed to one of the Store methods, // the session will be used for database access. func ContextWithSession(ctx context.Context, session *mgo.Session) context.Context { return context.WithValue(ctx, sessionKey{}, session) } simplekv-1.1.0/mgosimplekv/kv_test.go000066400000000000000000000014171405537713200177200ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package mgosimplekv_test import ( "fmt" "sync/atomic" "testing" "github.com/juju/mgotest" "github.com/juju/simplekv" "github.com/juju/simplekv/internal/simplekvtest" "github.com/juju/simplekv/mgosimplekv" errgo "gopkg.in/errgo.v1" ) func TestMgoStore(t *testing.T) { db, err := mgotest.New() if err != nil { if errgo.Cause(err) == mgotest.ErrDisabled { t.Skip(err) } t.Fatal(err) } defer db.Close() var id int32 simplekvtest.TestStore(t, func() (_ simplekv.Store, err error) { coll := fmt.Sprintf("test%d", atomic.AddInt32(&id, 1)) store, err := mgosimplekv.NewStore(db.C(coll)) if err != nil { return nil, errgo.Mask(err) } return store, nil }) } simplekv-1.1.0/sqlsimplekv/000077500000000000000000000000001405537713200157245ustar00rootroot00000000000000simplekv-1.1.0/sqlsimplekv/driver.go000066400000000000000000000061621405537713200175530ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package sqlsimplekv import ( "bytes" "context" "database/sql" "strings" "text/template" errgo "gopkg.in/errgo.v1" ) type tmplID int const ( _ tmplID = iota - 1 tmplGetKeyValue tmplGetKeyValueForUpdate tmplInsertKeyValue tmplListKeys numTmpl ) type queryer interface { ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row } // argBuilder is an interface that can be embedded in template parameters // to record the arguments needed to be supplied with SQL queries. type argBuilder interface { // Arg is a method that is called in templates with the value of // the next argument to be used in the query. Arg should remmebre // the value and return a valid placeholder to access that // argument when executing the query. Arg(interface{}) string // args returns the slice of arguments that should be used when // executing the query. args() []interface{} } type driver struct { tmpls [numTmpl]*template.Template argBuilderFunc func() argBuilder isDuplicate func(error) bool } // exec performs the Exec method on the given queryer by processing the // given template with the given params to determine the query to // execute. func (d *driver) exec(ctx context.Context, q queryer, tmplID tmplID, params argBuilder) (sql.Result, error) { query, err := d.executeTemplate(tmplID, params) if err != nil { return nil, errgo.Notef(err, "cannot build query") } res, err := q.ExecContext(ctx, query, params.args()...) return res, errgo.Mask(err, errgo.Any) } // query performs the Query method on the given queryer by processing the // given template with the given params to determine the query to // execute. func (d *driver) query(ctx context.Context, q queryer, tmplID tmplID, params argBuilder) (*sql.Rows, error) { query, err := d.executeTemplate(tmplID, params) if err != nil { return nil, errgo.Notef(err, "cannot build query") } rows, err := q.QueryContext(ctx, query, params.args()...) return rows, errgo.Mask(err, errgo.Any) } // queryRow performs the QueryRow method on the given queryer by // processing the given template with the given params to determine the // query to execute. func (d *driver) queryRow(ctx context.Context, q queryer, tmplID tmplID, params argBuilder) (*sql.Row, error) { query, err := d.executeTemplate(tmplID, params) if err != nil { return nil, errgo.Notef(err, "cannot build query") } return q.QueryRowContext(ctx, query, params.args()...), nil } func (d *driver) parseTemplate(tmplID tmplID, tmpl string) error { var err error d.tmpls[tmplID], err = template.New("").Funcs(template.FuncMap{ "join": strings.Join, }).Parse(tmpl) return errgo.Mask(err) } func (d *driver) executeTemplate(tmplID tmplID, params argBuilder) (string, error) { var buf bytes.Buffer if err := d.tmpls[tmplID].Execute(&buf, params); err != nil { return "", errgo.Mask(err) } return buf.String(), nil } simplekv-1.1.0/sqlsimplekv/keyvalue.go000066400000000000000000000126651405537713200201120ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package sqlsimplekv import ( "context" "database/sql" "fmt" "time" errgo "gopkg.in/errgo.v1" "github.com/juju/simplekv" ) // NewStore returns a new Store instance that uses the // given sql database for storage, generating SQL with the // given driver (currently only "postgres" is supported). // // The data will be stored in a table with the given name // (other SQL artificacts may also be created using the name as a prefix). func NewStore(driverName string, db *sql.DB, tableName string) (simplekv.Store, error) { if driverName != "postgres" { return nil, errgo.Newf("unsupported database driver %q", driverName) } driver, err := newPostgresDriver(db, tableName) if err != nil { return nil, errgo.Notef(err, "cannot initialise database") } return &kvStore{ tableName: tableName, db: db, driver: driver, }, nil } // A kvStore implements simplekv.Store. type kvStore struct { db *sql.DB driver *driver tableName string } // Context implements simplekv.Store.Context. func (s *kvStore) Context(ctx context.Context) (context.Context, func()) { return ctx, func() {} } type keyValueParams struct { argBuilder TableName string Key string Value []byte Expire sql.NullTime Update bool } // Get implements simplekv.Store.Get by selecting the blob with the // given key from the table. func (s *kvStore) Get(ctx context.Context, key string) ([]byte, error) { v, err := s.get(ctx, s.db, key, false) if err != nil { return nil, errgo.Mask(err, errgo.Is(simplekv.ErrNotFound)) } return v, nil } // get is like Get except that it operates on a general queryer value. // If forUpdate is true, it takes out a lock on the given key so that a subsequent // call to set will happen atomically. func (s *kvStore) get(ctx context.Context, q queryer, key string, forUpdate bool) ([]byte, error) { params := &keyValueParams{ argBuilder: s.driver.argBuilderFunc(), TableName: s.tableName, Key: key, } var value []byte tmpl := tmplGetKeyValue if forUpdate { tmpl = tmplGetKeyValueForUpdate } row, err := s.driver.queryRow(ctx, q, tmpl, params) if err != nil { return nil, errgo.Mask(err) } if err := row.Scan(&value); err != nil { if errgo.Cause(err) == sql.ErrNoRows { return nil, simplekv.KeyNotFoundError(key) } return nil, errgo.Mask(err) } return value, nil } // Set implements simplekv.Store.Set by upserting the blob with the // given key, value and expire time into the table. func (s *kvStore) Set(ctx context.Context, key string, value []byte, expire time.Time) error { return s.set(ctx, s.db, key, value, expire, false) } // set is like Set except that it operates on a general queryer value. // If insertOnly is true, the value will only be set if the key doesn't exist. func (s *kvStore) set(ctx context.Context, q queryer, key string, value []byte, expire time.Time, insertOnly bool) error { _, err := s.driver.exec(ctx, q, tmplInsertKeyValue, &keyValueParams{ argBuilder: s.driver.argBuilderFunc(), TableName: s.tableName, Key: key, Value: value, Expire: sql.NullTime{ Time: expire, Valid: !expire.IsZero(), }, Update: !insertOnly, }) if err != nil { return errgo.Mask(err, s.driver.isDuplicate) } return nil } // Update implements simplekv.Store.Update. func (s *kvStore) Update(ctx context.Context, key string, expire time.Time, getVal func(old []byte) ([]byte, error)) error { for { insertOnly := false err := s.withTx(func(tx *sql.Tx) error { v, err := s.get(ctx, tx, key, true) if err != nil { if errgo.Cause(err) != simplekv.ErrNotFound { return errgo.Mask(err) } // The document doesn't exist, so we want to fail if some other process // has inserted it concurrently. insertOnly = true } else if v == nil { v = []byte{} } newVal, err := getVal(v) if err != nil { return errgo.Mask(err, errgo.Any) } err = s.set(ctx, tx, key, newVal, expire, insertOnly) if err == nil { return nil } return errgo.Mask(err, s.driver.isDuplicate) }) if !insertOnly || !s.driver.isDuplicate(errgo.Cause(err)) { return errgo.Mask(err, errgo.Any) } // The document didn't previously exist (so we couldn't lock it) but when we // tried the insert, it failed with a duplicate-key error and aborted the transaction, // so we'll now try again with the document in place. } } // Keys implements simplekv.Store.Keys. func (s *kvStore) Keys(ctx context.Context) ([]string, error) { rows, err := s.driver.query(ctx, s.db, tmplListKeys, &keyValueParams{ argBuilder: s.driver.argBuilderFunc(), TableName: s.tableName, }) if err != nil { return nil, errgo.Mask(err) } defer rows.Close() keys := []string{} for rows.Next() { var key string if err := rows.Scan(&key); err != nil { return nil, errgo.Mask(err) } keys = append(keys, key) } if err := rows.Err(); err != nil { return nil, errgo.Mask(err) } return keys, nil } // withTx runs f in a new transaction. any error returned by f will not // have it's cause masked. func (s *kvStore) withTx(f func(*sql.Tx) error) error { tx, err := s.db.Begin() if err != nil { return errgo.Mask(err) } if err := f(tx); err != nil { if err1 := tx.Rollback(); err1 != nil { return errgo.NoteMask(err, fmt.Sprintf("failed to roll back (error: %v) after error", err1), errgo.Any) } return errgo.Mask(err, errgo.Any) } return errgo.Mask(tx.Commit()) } simplekv-1.1.0/sqlsimplekv/postgres.go000066400000000000000000000054621405537713200201300ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package sqlsimplekv import ( "bytes" "database/sql" "fmt" "text/template" "github.com/lib/pq" errgo "gopkg.in/errgo.v1" ) const postgresInitTmpl = ` CREATE TABLE IF NOT EXISTS {{.TableName}} ( key TEXT NOT NULL, value BYTEA NOT NULL, expire TIMESTAMP WITH TIME ZONE, UNIQUE (key) ); CREATE OR REPLACE FUNCTION {{.TableName}}_expire_fn() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN DELETE FROM {{.TableName}} WHERE expire < NOW(); RETURN NEW; END; $$; CREATE INDEX IF NOT EXISTS {{.TableName}}_expire ON {{.TableName}} (expire); DROP TRIGGER IF EXISTS {{.TableName}}_expire_tr ON {{.TableName}}; CREATE TRIGGER {{.TableName}}_expire_tr BEFORE INSERT ON {{.TableName}} EXECUTE PROCEDURE {{.TableName}}_expire_fn(); ` var postgresTmpls = [numTmpl]string{ tmplGetKeyValue: ` SELECT value FROM {{.TableName}} WHERE key={{.Key | .Arg}} AND (expire IS NULL OR expire > now())`, tmplGetKeyValueForUpdate: ` SELECT value FROM {{.TableName}} WHERE key={{.Key | .Arg}} AND (expire IS NULL OR expire > now()) FOR UPDATE`, tmplInsertKeyValue: ` INSERT INTO {{.TableName}} (key, value, expire) VALUES ({{.Key | .Arg}}, {{.Value | .Arg}}, {{.Expire | .Arg}}) {{if .Update}}ON CONFLICT (key) DO UPDATE SET value={{.Value | .Arg}}, expire={{.Expire | .Arg}}{{end}}`, tmplListKeys: ` SELECT DISTINCT key FROM {{.TableName}} WHERE (expire IS NULL OR expire > now()) `, } // newPostgresDriver creates a postgres driver using the given DB. func newPostgresDriver(db *sql.DB, tableName string) (*driver, error) { tmpl, err := template.New("").Parse(postgresInitTmpl) if err != nil { return nil, errgo.Mask(err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, keyValueParams{ TableName: tableName, }); err != nil { return nil, errgo.Mask(err) } if _, err := db.Exec(buf.String()); err != nil { return nil, errgo.Mask(err) } d := &driver{ argBuilderFunc: func() argBuilder { return &postgresArgBuilder{} }, isDuplicate: postgresIsDuplicate, } for i, t := range postgresTmpls { if err := d.parseTemplate(tmplID(i), t); err != nil { return nil, errgo.Notef(err, "cannot parse template %v", t) } } return d, nil } func postgresIsDuplicate(err error) bool { if pqerr, ok := err.(*pq.Error); ok && pqerr.Code.Name() == "unique_violation" { return true } return false } // postgresArgBuilder implements an argBuilder that produces placeholders // in the the "$n" format. type postgresArgBuilder struct { args_ []interface{} } // Arg implements argbuilder.Arg. func (b *postgresArgBuilder) Arg(a interface{}) string { b.args_ = append(b.args_, a) return fmt.Sprintf("$%d", len(b.args_)) } // args implements argbuilder.args. func (b *postgresArgBuilder) args() []interface{} { return b.args_ } simplekv-1.1.0/sqlsimplekv/postgres_test.go000066400000000000000000000013401405537713200211560ustar00rootroot00000000000000// Copyright 2018 Canonical Ltd. // Licensed under the LGPL, see LICENCE file for details. package sqlsimplekv_test import ( "fmt" "sync/atomic" "testing" "github.com/juju/postgrestest" errgo "gopkg.in/errgo.v1" "github.com/juju/simplekv" "github.com/juju/simplekv/internal/simplekvtest" "github.com/juju/simplekv/sqlsimplekv" ) func TestPostgresStore(t *testing.T) { pg, err := postgrestest.New() if err != nil { if errgo.Cause(err) == postgrestest.ErrDisabled { t.Skip(err) } t.Fatal(err) } defer pg.Close() var id int32 simplekvtest.TestStore(t, func() (_ simplekv.Store, err error) { table := fmt.Sprintf("test%d", atomic.AddInt32(&id, 1)) return sqlsimplekv.NewStore("postgres", pg.DB, table) }) }