From 8aeed097861fa1ab32f9ffe9e415d30f5705b23c Mon Sep 17 00:00:00 2001 From: Johnny Steenbergen Date: Tue, 23 Jan 2024 17:22:30 -0600 Subject: [PATCH] chore(allsrv): add sqlite db implementation and test suite The test suite underwent a few changes here. There are race conditions in the original test suite, when it comes to executing the concurrency focused tests. Made a small update here to address that. TL/DR the race is with the actual `*testing.T` type, so we make use of the closure to capture the error and log. This enforces the `testing.T` access is done **after** any test behavior. We are also able to update the error handling a bit here. I don't care much for what the error message says, but I care deeply about the behavior of the errors I receive. I want to validate the correct behavior is obtained. This is very useful when integrating amongst a larger, more complex system. For this trivial `Foo` server, we don't have much complexity. The sqlite db implementation here is fairly trivial once again. We're able to reuse the test suite in full. All that was required is a new funciton to initize the unit under test (UUT), and the rest is including 4 more lines of code to call the test around it. Not to shabby. Since we've effectively decoupled our domain `Foo` from the db entity `foo`, we've provided maximum flexibility to our database implementation without having to pollute the domain space. This is intensely useful as a system grows. Think through, what would it look like to add a `PostgreSQL` db implementation? Not as much now that you have a test suite to verify the desired behavior. The last thing that is missing here is what we do to decouple our server from HTTP. There is a glaring hole in our design, and that's the lack of service layer. The layer where all our business logic resides. Take a moment to think through what this might look like. Question: How would you break up the server so that it's no longer coupled to HTTP/REST? Question: What can we do to allow ourselves to support any myriad of `RPC` technologies without duplicating allthe business logic? --- allsrv/cmd/allsrv/main.go | 52 +++++++- allsrv/db_inmem.go | 2 +- allsrv/db_sqlite.go | 121 ++++++++++++++++++ allsrv/db_sqlite_test.go | 50 ++++++++ allsrv/db_test.go | 115 ++++++++++------- allsrv/errors.go | 16 ++- allsrv/migrations/migrations.go | 15 +++ .../migrations/sqlite/0001_genesis.down.sql | 1 + allsrv/migrations/sqlite/0001_genesis.up.sql | 8 ++ allsrv/server.go | 2 +- go.mod | 9 ++ go.sum | 25 ++++ 12 files changed, 362 insertions(+), 54 deletions(-) create mode 100644 allsrv/db_sqlite.go create mode 100644 allsrv/db_sqlite_test.go create mode 100644 allsrv/migrations/migrations.go create mode 100644 allsrv/migrations/sqlite/0001_genesis.down.sql create mode 100644 allsrv/migrations/sqlite/0001_genesis.up.sql diff --git a/allsrv/cmd/allsrv/main.go b/allsrv/cmd/allsrv/main.go index d8617b7..7e295b4 100644 --- a/allsrv/cmd/allsrv/main.go +++ b/allsrv/cmd/allsrv/main.go @@ -1,15 +1,31 @@ package main import ( + "database/sql" "log" "net/http" "os" + "github.com/golang-migrate/migrate/v4" + migsqlite "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "github.com/jsteenb2/mess/allsrv" + "github.com/jsteenb2/mess/allsrv/migrations" ) func main() { - db := new(allsrv.InmemDB) + var db allsrv.DB = new(allsrv.InmemDB) + if dsn := os.Getenv("ALLSRV_SQLITE_DSN"); dsn != "" { + var err error + db, err = newSQLiteDB(dsn) + if err != nil { + log.Println("failed to open sqlite db: " + err.Error()) + os.Exit(1) + } + } var svr http.Handler switch os.Getenv("ALLSRV_SERVER") { @@ -34,3 +50,37 @@ func main() { os.Exit(1) } } + +func newSQLiteDB(dsn string) (allsrv.DB, error) { + const driver = "sqlite3" + + db, err := sql.Open(driver, dsn) + if err != nil { + return nil, err + } + + const dbName = "testdb" + drvr, err := migsqlite.WithInstance(db, &migsqlite.Config{DatabaseName: dbName}) + if err != nil { + return nil, err + } + + iodrvr, err := iofs.New(migrations.SQLite, "sqlite") + if err != nil { + return nil, err + } + + m, err := migrate.NewWithInstance("iofs", iodrvr, dbName, drvr) + if err != nil { + return nil, err + } + err = m.Up() + if err != nil { + return nil, err + } + + dbx := sqlx.NewDb(db, driver) + dbx.SetMaxIdleConns(1) + + return allsrv.NewSQLiteDB(dbx), nil +} diff --git a/allsrv/db_inmem.go b/allsrv/db_inmem.go index 1b36f48..d70954e 100644 --- a/allsrv/db_inmem.go +++ b/allsrv/db_inmem.go @@ -16,7 +16,7 @@ func (db *InmemDB) CreateFoo(_ context.Context, f Foo) error { defer db.mu.Unlock() for _, existing := range db.m { - if f.Name == existing.Name { + if f.Name == existing.Name || f.ID == existing.ID { return ExistsErr("foo "+f.Name+" exists", "name", f.Name, "existing_foo_id", existing.ID) // 8) } } diff --git a/allsrv/db_sqlite.go b/allsrv/db_sqlite.go new file mode 100644 index 0000000..3f45351 --- /dev/null +++ b/allsrv/db_sqlite.go @@ -0,0 +1,121 @@ +package allsrv + +import ( + "context" + "database/sql" + "errors" + "sync" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + "github.com/mattn/go-sqlite3" +) + +// NewSQLiteDB creates a new sqlite db. +func NewSQLiteDB(db *sqlx.DB) DB { + return &sqlDB{ + db: db, + sq: sq.StatementBuilder.PlaceholderFormat(sq.Question), + } +} + +type sqlDB struct { + db *sqlx.DB + sq sq.StatementBuilderType + + mu sync.RWMutex +} + +func (s *sqlDB) CreateFoo(ctx context.Context, f Foo) error { + sb := s.sq. + Insert("foos"). + Columns("id", "name", "note", "created_at", "updated_at"). + Values(f.ID, f.Name, f.Note, f.CreatedAt, f.UpdatedAt) + + _, err := s.exec(ctx, sb) + return err +} + +func (s *sqlDB) ReadFoo(ctx context.Context, id string) (Foo, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + const query = `SELECT * FROM foos WHERE id=?` + + var ent entFoo + err := s.db.GetContext(ctx, &ent, s.db.Rebind(query), id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return Foo{}, NotFoundErr("foo not found for id: " + id) + } + return Foo{}, err + } + + out := Foo{ + ID: ent.ID, + Name: ent.Name, + Note: ent.Note, + CreatedAt: ent.CreatedAt, + UpdatedAt: ent.UpdatedAt, + } + + return out, nil +} + +func (s *sqlDB) UpdateFoo(ctx context.Context, f Foo) error { + sb := s.sq. + Update("foos"). + Set("name", f.Name). + Set("note", f.Note). + Set("updated_at", f.UpdatedAt). + Where(sq.Eq{"id": f.ID}) + + return s.update(ctx, sb) +} + +func (s *sqlDB) DelFoo(ctx context.Context, id string) error { + err := s.update(ctx, s.sq.Delete("foos").Where(sq.Eq{"id": id})) + return err +} + +func (s *sqlDB) exec(ctx context.Context, sqlizer sq.Sqlizer) (sql.Result, error) { + query, args, err := sqlizer.ToSql() + if err != nil { + return nil, err + } + + s.mu.Lock() + defer s.mu.Unlock() + + res, err := s.db.ExecContext(ctx, query, args...) + if sqErr := new(sqlite3.Error); errors.As(err, sqErr) { + switch sqErr.Code { + case sqlite3.ErrConstraint: + return nil, ExistsErr("foo exists") + } + } + return res, err +} + +func (s *sqlDB) update(ctx context.Context, sqlizer sq.Sqlizer) error { + res, err := s.exec(ctx, sqlizer) + if err != nil { + return err + } + + n, err := res.RowsAffected() + if err == nil && n == 0 { + return NotFoundErr("foo not found") + } + + return nil +} + +type entFoo struct { + ID string `db:"id"` + Name string `db:"name"` + Note string `db:"note"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/allsrv/db_sqlite_test.go b/allsrv/db_sqlite_test.go new file mode 100644 index 0000000..6a17407 --- /dev/null +++ b/allsrv/db_sqlite_test.go @@ -0,0 +1,50 @@ +package allsrv_test + +import ( + "database/sql" + "testing" + + "github.com/golang-migrate/migrate/v4" + migsqlite "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jsteenb2/mess/allsrv" + "github.com/jsteenb2/mess/allsrv/migrations" +) + +func TestSQLite(t *testing.T) { + testDB(t, func(t *testing.T) allsrv.DB { + db := newSQLiteInmem(t) + t.Cleanup(func() { + assert.NoError(t, db.Close()) + }) + + return allsrv.NewSQLiteDB(db) + }) +} + +func newSQLiteInmem(t *testing.T) *sqlx.DB { + const driver = "sqlite3" + + db, err := sql.Open(driver, ":memory:") + require.NoError(t, err) + + const dbName = "testdb" + drvr, err := migsqlite.WithInstance(db, &migsqlite.Config{DatabaseName: dbName}) + require.NoError(t, err) + + iodrvr, err := iofs.New(migrations.SQLite, "sqlite") + require.NoError(t, err) + + m, err := migrate.NewWithInstance("iofs", iodrvr, dbName, drvr) + require.NoError(t, err) + require.NoError(t, m.Up()) + + dbx := sqlx.NewDb(db, driver) + dbx.SetMaxIdleConns(1) + return dbx +} diff --git a/allsrv/db_test.go b/allsrv/db_test.go index c86e9a1..5fdaaf5 100644 --- a/allsrv/db_test.go +++ b/allsrv/db_test.go @@ -3,6 +3,7 @@ package allsrv_test import ( "context" "errors" + "sync" "testing" "time" @@ -105,12 +106,10 @@ func testDBCreateFoo(t *testing.T, initFn dbInitFn) { } } - // execute while rest of test completes - for _, f := range []allsrv.Foo{newFoo("1"), newFoo("2"), newFoo("3"), newFoo("4"), newFoo("5")} { - go func(f allsrv.Foo) { - require.NoError(t, db.CreateFoo(context.TODO(), f)) - }(f) - } + fixtures := []allsrv.Foo{newFoo("1"), newFoo("2"), newFoo("3"), newFoo("4"), newFoo("5")} + doConcurrent(t, fixtures, func(f allsrv.Foo) error { + return db.CreateFoo(context.TODO(), f) + }) }, inputs: inputs{ foo: allsrv.Foo{ @@ -153,22 +152,23 @@ func testDBCreateFoo(t *testing.T, initFn dbInitFn) { require.Error(t, insertErr) }, }, - // { - // name: "with foo containing ID that already exists should fail", - // prepare: prepDBFoos(allsrv.Foo{ID: "1", Name: "name-1"}), - // inputs: inputs{ - // foo: allsrv.Foo{ - // ID: "1", - // Name: "name-2", - // Note: "some note", - // CreatedAt: start, - // UpdatedAt: start, - // }, - // }, - // want: func(t *testing.T, db allsrv.DB, insertErr error) { - // require.Error(t, insertErr) - // }, - // }, + { + name: "with foo containing ID that already exists should fail", + prepare: createFoos(allsrv.Foo{ID: "1", Name: "name-1"}), + inputs: inputs{ + foo: allsrv.Foo{ + ID: "1", + Name: "name-2", + Note: "some note", + CreatedAt: start, + UpdatedAt: start, + }, + }, + want: func(t *testing.T, db allsrv.DB, insertErr error) { + require.Error(t, insertErr) + assert.True(t, allsrv.IsExistsErr(insertErr)) + }, + }, } for _, tt := range tests { @@ -255,11 +255,10 @@ func testDBReadFoo(t *testing.T, initFn dbInitFn) { } // execute these while test is executing read - for _, f := range []allsrv.Foo{newFoo("a"), newFoo("b"), newFoo("c"), newFoo("d"), newFoo("e")} { - go func(f allsrv.Foo) { - require.NoError(t, db.UpdateFoo(context.TODO(), f)) - }(f) - } + fixtures := []allsrv.Foo{newFoo("a"), newFoo("b"), newFoo("c"), newFoo("d"), newFoo("e")} + doConcurrent(t, fixtures, func(f allsrv.Foo) error { + return db.UpdateFoo(context.TODO(), f) + }) }, inputs: inputs{ id: "1", @@ -371,11 +370,10 @@ func testDBUpdateFoo(t *testing.T, initFn dbInitFn) { } } - for _, f := range []allsrv.Foo{newFoo("a"), newFoo("b"), newFoo("c"), newFoo("d"), newFoo("e")} { - go func(f allsrv.Foo) { - require.NoError(t, db.UpdateFoo(context.TODO(), f)) - }(f) - } + fixtures := []allsrv.Foo{newFoo("a"), newFoo("b"), newFoo("c"), newFoo("d"), newFoo("e")} + doConcurrent(t, fixtures, func(f allsrv.Foo) error { + return db.UpdateFoo(context.TODO(), f) + }) }, inputs: inputs{ foo: allsrv.Foo{ @@ -407,11 +405,7 @@ func testDBUpdateFoo(t *testing.T, initFn dbInitFn) { }, want: func(t *testing.T, db allsrv.DB, updateErr error) { require.Error(t, updateErr) - - // this is pretty gross, we're matching against a raw error/text value - // any change in the error message means we have to update tests too - want := errors.New("foo not found for id: 1") - assert.Equal(t, want.Error(), updateErr.Error()) + assert.True(t, allsrv.IsNotFoundErr(updateErr)) }, }, } @@ -482,16 +476,15 @@ func testDBDeleteFoo(t *testing.T, initFn dbInitFn) { } } - for _, f := range []allsrv.Foo{newFoo("1"), newFoo("2"), newFoo("3"), newFoo("4"), newFoo("5")} { + fixtures := []allsrv.Foo{newFoo("1"), newFoo("2"), newFoo("3"), newFoo("4"), newFoo("5")} + for _, f := range fixtures { require.NoError(t, db.CreateFoo(context.TODO(), f)) } - // leave the foo "1" del for the input - for _, id := range []string{"2", "3", "4", "5"} { - go func(id string) { - require.NoError(t, db.DelFoo(context.TODO(), id)) - }(id) - } + fixtures = fixtures[1:] + doConcurrent(t, fixtures, func(f allsrv.Foo) error { + return db.DelFoo(context.TODO(), f.ID) + }) }, inputs: inputs{id: "1"}, want: func(t *testing.T, db allsrv.DB, delErr error) { @@ -503,11 +496,7 @@ func testDBDeleteFoo(t *testing.T, initFn dbInitFn) { inputs: inputs{id: "1"}, want: func(t *testing.T, db allsrv.DB, delErr error) { require.Error(t, delErr) - - // this is pretty gross, we're matching against a raw error/text value - // any change in the error message means we have to update tests too - want := errors.New("foo not found for id: 1") - assert.Equal(t, want.Error(), delErr.Error()) + assert.True(t, allsrv.IsNotFoundErr(delErr)) }, }, } @@ -528,3 +517,31 @@ func testDBDeleteFoo(t *testing.T, initFn dbInitFn) { }) } } + +func doConcurrent(t *testing.T, foos []allsrv.Foo, doFn func(f allsrv.Foo) error) { + t.Helper() + + // execute while rest of test completes + errStr, wg := make(chan error, len(foos)), new(sync.WaitGroup) + t.Cleanup(func() { + t.Helper() + + wg.Wait() + close(errStr) + + for err := range errStr { + if err == nil { + continue + } + assert.NoError(t, err) + } + }) + + for _, f := range foos { + wg.Add(1) + go func(f allsrv.Foo) { + defer wg.Done() + errStr <- doFn(f) + }(f) + } +} diff --git a/allsrv/errors.go b/allsrv/errors.go index dc53d51..fd35a74 100644 --- a/allsrv/errors.go +++ b/allsrv/errors.go @@ -1,5 +1,9 @@ package allsrv +import ( + "errors" +) + const ( errTypeUnknown = iota errTypeExists @@ -41,7 +45,15 @@ func NotFoundErr(msg string, fields ...any) error { } } +func IsNotFoundErr(err error) bool { + return isErrType(err, errTypeNotFound) +} + +func IsExistsErr(err error) bool { + return isErrType(err, errTypeExists) +} + func isErrType(err error, want int) bool { - e, _ := err.(Err) - return err != nil && e.Type == want + var aErr Err + return errors.As(err, &aErr) && aErr.Type == want } diff --git a/allsrv/migrations/migrations.go b/allsrv/migrations/migrations.go new file mode 100644 index 0000000..a5c7937 --- /dev/null +++ b/allsrv/migrations/migrations.go @@ -0,0 +1,15 @@ +// package migrations provides for a one-stop shop for accessing migrations. +// this removes the need to worry about relative pathing from the source file +// intending to migrate a DB. All that is required is a go import and a +// reference to the migrations of interest. + +package migrations + +import ( + "embed" +) + +// SQLite represents the sqlite migration files. +// +//go:embed sqlite +var SQLite embed.FS diff --git a/allsrv/migrations/sqlite/0001_genesis.down.sql b/allsrv/migrations/sqlite/0001_genesis.down.sql new file mode 100644 index 0000000..cea28b4 --- /dev/null +++ b/allsrv/migrations/sqlite/0001_genesis.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS foos; \ No newline at end of file diff --git a/allsrv/migrations/sqlite/0001_genesis.up.sql b/allsrv/migrations/sqlite/0001_genesis.up.sql new file mode 100644 index 0000000..f4a7dc8 --- /dev/null +++ b/allsrv/migrations/sqlite/0001_genesis.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE foos +( + id TEXT PRIMARY KEY, + name TEXT UNIQUE, + note TEXT, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL +); \ No newline at end of file diff --git a/allsrv/server.go b/allsrv/server.go index a73bbad..5b192cb 100644 --- a/allsrv/server.go +++ b/allsrv/server.go @@ -26,7 +26,7 @@ import ( ✅5) no tests a) how do we ensure things work? b) how do we know what is intended by the current implementation? - 6) http/db are coupled to the same type + ✅6) http/db are coupled to the same type a) what happens when the concerns diverge? aka http wants a shape the db does not? (note: it happens A LOT) 7) Server only works with HTTP a) what happens when we want to support grpc? thrift? other protocol? diff --git a/go.mod b/go.mod index d5da4f1..dd608ef 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,28 @@ module github.com/jsteenb2/mess go 1.22 require ( + github.com/Masterminds/squirrel v1.5.4 github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang-migrate/migrate/v4 v4.17.0 github.com/hashicorp/go-metrics v0.5.3 + github.com/jmoiron/sqlx v1.3.5 + github.com/mattn/go-sqlite3 v1.14.19 github.com/opentracing/opentracing-go v1.2.0 github.com/stretchr/testify v1.8.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v0.5.0 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + go.uber.org/atomic v1.7.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 69503c8..a30eff5 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -17,26 +19,37 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -50,6 +63,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -89,6 +112,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=