From c81c996fa04ca0c2f44b56ea5b7155991d6237ca Mon Sep 17 00:00:00 2001 From: Antonio Pagano <645522+paganotoni@users.noreply.github.com> Date: Wed, 27 Jan 2021 15:58:44 -0500 Subject: [PATCH] Latest from development (#617) * fix: improve model ID field customization (#604) Updates places where `"id"` was hardcoded instead of using `model.IDField()`. * Ensure uninitialized map is initialized when unmarshaling json Add tests for this scenario * exclude migration_table_name from connection string * add test for OptionsString * Add support for pointer FKs when preloading a belongs_to association (#602) * feat: support context-aware tablenames (#614) This patch adds a feature which enables pop to pass down the connection context to the model's TableName() function by implementing TableName(ctx context.Context) string. The context can be used to dynamically generate tablenames which can be important for prefixed or generic tables and other use cases. * Bump pg deps (#616) * Reset to development * bumping pgx and pgconn versions Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan Co-authored-by: Patrik Co-authored-by: Michael Montgomery Co-authored-by: kyrozetera Co-authored-by: Reggie Riser <4960757+reggieriser@users.noreply.github.com> Co-authored-by: hackerman <3372410+aeneasr@users.noreply.github.com> Co-authored-by: Stanislas Michalak Co-authored-by: Larry M Jordan --- belongs_to.go | 8 +- belongs_to_test.go | 7 +- columns/columns.go | 14 ++- columns/columns_for_struct.go | 10 +- columns/columns_test.go | 46 +++++++- columns/readable_columns_test.go | 6 +- columns/writeable_columns_test.go | 8 +- connection.go | 10 ++ connection_details.go | 7 +- connection_details_test.go | 19 +++ connection_instrumented.go | 1 + connection_instrumented_nosqlite_test.go | 3 +- connection_instrumented_test.go | 5 +- dialect_sqlite.go | 3 +- executors.go | 38 +++--- executors_test.go | 70 +++++++++++ finders.go | 14 +-- finders_test.go | 2 +- go.mod | 4 +- go.sum | 82 +++++++------ match_test.go | 3 +- migration_info_test.go | 3 +- model.go | 107 ++++++++++++----- model_context_test.go | 109 +++++++++++++++++ model_test.go | 111 ++++++++++++++---- pop_test.go | 23 ++-- preload_associations.go | 19 ++- preload_associations_test.go | 33 ++++++ query_test.go | 15 +-- scopes_test.go | 3 +- slices/map.go | 3 + slices/map_test.go | 11 ++ soda/cmd/migrate_status.go | 3 +- sql_builder.go | 4 +- store.go | 4 + .../migrations/20181104135856_taxis.up.fizz | 3 +- .../20201028153041_non_standard_id.down.fizz | 1 + .../20201028153041_non_standard_id.up.fizz | 6 + .../20210104145901_context_tables.down.fizz | 2 + .../20210104145901_context_tables.up.fizz | 9 ++ testdata/models/ac/user.go | 9 ++ testdata/models/bc/user.go | 9 ++ validations.go | 2 +- 43 files changed, 664 insertions(+), 185 deletions(-) create mode 100644 model_context_test.go create mode 100644 testdata/migrations/20201028153041_non_standard_id.down.fizz create mode 100644 testdata/migrations/20201028153041_non_standard_id.up.fizz create mode 100644 testdata/migrations/20210104145901_context_tables.down.fizz create mode 100644 testdata/migrations/20210104145901_context_tables.up.fizz create mode 100644 testdata/models/ac/user.go create mode 100644 testdata/models/bc/user.go diff --git a/belongs_to.go b/belongs_to.go index 0b5c977e..d261a315 100644 --- a/belongs_to.go +++ b/belongs_to.go @@ -19,7 +19,7 @@ func (c *Connection) BelongsToAs(model interface{}, as string) *Query { // BelongsTo adds a "where" clause based on the "ID" of the // "model" passed into it. func (q *Query) BelongsTo(model interface{}) *Query { - m := &Model{Value: model} + m := NewModel(model, q.Connection.Context()) q.Where(fmt.Sprintf("%s = ?", m.associationName()), m.ID()) return q } @@ -27,7 +27,7 @@ func (q *Query) BelongsTo(model interface{}) *Query { // BelongsToAs adds a "where" clause based on the "ID" of the // "model" passed into it, using an alias. func (q *Query) BelongsToAs(model interface{}, as string) *Query { - m := &Model{Value: model} + m := NewModel(model, q.Connection.Context()) q.Where(fmt.Sprintf("%s = ?", as), m.ID()) return q } @@ -42,8 +42,8 @@ func (c *Connection) BelongsToThrough(bt, thru interface{}) *Query { // through the associated "thru" model. func (q *Query) BelongsToThrough(bt, thru interface{}) *Query { q.belongsToThroughClauses = append(q.belongsToThroughClauses, belongsToThroughClause{ - BelongsTo: &Model{Value: bt}, - Through: &Model{Value: thru}, + BelongsTo: NewModel(bt, q.Connection.Context()), + Through: NewModel(thru, q.Connection.Context()), }) return q } diff --git a/belongs_to_test.go b/belongs_to_test.go index e4e3a3e7..9eb99914 100644 --- a/belongs_to_test.go +++ b/belongs_to_test.go @@ -1,6 +1,7 @@ package pop import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -14,7 +15,7 @@ func Test_BelongsTo(t *testing.T) { q := PDB.BelongsTo(&User{ID: 1}) - m := &Model{Value: &Enemy{}} + m := NewModel(new(Enemy), context.Background()) sql, _ := q.ToSQL(m) r.Equal(ts("SELECT enemies.A FROM enemies AS enemies WHERE user_id = ?"), sql) @@ -28,7 +29,7 @@ func Test_BelongsToAs(t *testing.T) { q := PDB.BelongsToAs(&User{ID: 1}, "u_id") - m := &Model{Value: &Enemy{}} + m := NewModel(new(Enemy), context.Background()) sql, _ := q.ToSQL(m) r.Equal(ts("SELECT enemies.A FROM enemies AS enemies WHERE u_id = ?"), sql) @@ -43,7 +44,7 @@ func Test_BelongsToThrough(t *testing.T) { q := PDB.BelongsToThrough(&User{ID: 1}, &Friend{}) qs := "SELECT enemies.A FROM enemies AS enemies, good_friends AS good_friends WHERE good_friends.user_id = ? AND enemies.id = good_friends.enemy_id" - m := &Model{Value: &Enemy{}} + m := NewModel(new(Enemy), context.Background()) sql, _ := q.ToSQL(m) r.Equal(ts(qs), sql) } diff --git a/columns/columns.go b/columns/columns.go index 8ee8f15c..4ea26564 100644 --- a/columns/columns.go +++ b/columns/columns.go @@ -13,6 +13,7 @@ type Columns struct { lock *sync.RWMutex TableName string TableAlias string + IDField string } // Add a column to the list. @@ -74,7 +75,7 @@ func (c *Columns) Add(names ...string) []*Column { } else if xs[1] == "w" { col.Readable = false } - } else if col.Name == "id" { + } else if col.Name == c.IDField { col.Writeable = false } @@ -98,7 +99,7 @@ func (c *Columns) Remove(names ...string) { // Writeable gets a list of the writeable columns from the column list. func (c Columns) Writeable() *WriteableColumns { - w := &WriteableColumns{NewColumnsWithAlias(c.TableName, c.TableAlias)} + w := &WriteableColumns{NewColumnsWithAlias(c.TableName, c.TableAlias, c.IDField)} for _, col := range c.Cols { if col.Writeable { w.Cols[col.Name] = col @@ -109,7 +110,7 @@ func (c Columns) Writeable() *WriteableColumns { // Readable gets a list of the readable columns from the column list. func (c Columns) Readable() *ReadableColumns { - w := &ReadableColumns{NewColumnsWithAlias(c.TableName, c.TableAlias)} + w := &ReadableColumns{NewColumnsWithAlias(c.TableName, c.TableAlias, c.IDField)} for _, col := range c.Cols { if col.Readable { w.Cols[col.Name] = col @@ -157,17 +158,18 @@ func (c Columns) SymbolizedString() string { } // NewColumns constructs a list of columns for a given table name. -func NewColumns(tableName string) Columns { - return NewColumnsWithAlias(tableName, "") +func NewColumns(tableName, idField string) Columns { + return NewColumnsWithAlias(tableName, "", idField) } // NewColumnsWithAlias constructs a list of columns for a given table // name, using a given alias for the table. -func NewColumnsWithAlias(tableName string, tableAlias string) Columns { +func NewColumnsWithAlias(tableName, tableAlias, idField string) Columns { return Columns{ lock: &sync.RWMutex{}, Cols: map[string]*Column{}, TableName: tableName, TableAlias: tableAlias, + IDField: idField, } } diff --git a/columns/columns_for_struct.go b/columns/columns_for_struct.go index 22cdbebc..a20cd426 100644 --- a/columns/columns_for_struct.go +++ b/columns/columns_for_struct.go @@ -6,17 +6,17 @@ import ( // ForStruct returns a Columns instance for // the struct passed in. -func ForStruct(s interface{}, tableName string) (columns Columns) { - return ForStructWithAlias(s, tableName, "") +func ForStruct(s interface{}, tableName, idField string) (columns Columns) { + return ForStructWithAlias(s, tableName, "", idField) } // ForStructWithAlias returns a Columns instance for the struct passed in. // If the tableAlias is not empty, it will be used. -func ForStructWithAlias(s interface{}, tableName string, tableAlias string) (columns Columns) { - columns = NewColumnsWithAlias(tableName, tableAlias) +func ForStructWithAlias(s interface{}, tableName, tableAlias, idField string) (columns Columns) { + columns = NewColumnsWithAlias(tableName, tableAlias, idField) defer func() { if r := recover(); r != nil { - columns = NewColumnsWithAlias(tableName, tableAlias) + columns = NewColumnsWithAlias(tableName, tableAlias, idField) columns.Add("*") } }() diff --git a/columns/columns_test.go b/columns/columns_test.go index caa0716a..f4699dc4 100644 --- a/columns/columns_test.go +++ b/columns/columns_test.go @@ -21,8 +21,8 @@ type foos []foo func Test_Column_MapsSlice(t *testing.T) { r := require.New(t) - c1 := columns.ForStruct(&foo{}, "foo") - c2 := columns.ForStruct(&foos{}, "foo") + c1 := columns.ForStruct(&foo{}, "foo", "id") + c2 := columns.ForStruct(&foos{}, "foo", "id") r.Equal(c1.String(), c2.String()) } @@ -30,7 +30,7 @@ func Test_Columns_Basics(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") r.Equal(len(c.Cols), 4) r.Equal(c.Cols["first_name"], &columns.Column{Name: "first_name", Writeable: false, Readable: true, SelectSQL: "first_name as f"}) r.Equal(c.Cols["LastName"], &columns.Column{Name: "LastName", Writeable: true, Readable: true, SelectSQL: "foo.LastName"}) @@ -43,7 +43,7 @@ func Test_Columns_Add(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") r.Equal(len(c.Cols), 4) c.Add("foo", "first_name") r.Equal(len(c.Cols), 5) @@ -55,7 +55,7 @@ func Test_Columns_Remove(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") r.Equal(len(c.Cols), 4) c.Remove("foo", "first_name") r.Equal(len(c.Cols), 3) @@ -75,9 +75,43 @@ func (fooQuoter) Quote(key string) string { func Test_Columns_Sorted(t *testing.T) { r := require.New(t) - c := columns.ForStruct(fooWithSuffix{}, "fooWithSuffix") + c := columns.ForStruct(fooWithSuffix{}, "fooWithSuffix", "id") r.Equal(len(c.Cols), 2) r.Equal(c.SymbolizedString(), ":amount, :amount_units") r.Equal(c.String(), "amount, amount_units") r.Equal(c.QuotedString(fooQuoter{}), "`amount`, `amount_units`") } + +func Test_Columns_IDField(t *testing.T) { + type withID struct { + ID string `db:"id"` + } + + r := require.New(t) + c := columns.ForStruct(withID{}, "with_id", "id") + r.Equal(1, len(c.Cols), "%+v", c) + r.Equal(&columns.Column{Name: "id", Writeable: false, Readable: true, SelectSQL: "with_id.id"}, c.Cols["id"]) +} + +func Test_Columns_IDField_Readonly(t *testing.T) { + type withIDReadonly struct { + ID string `db:"id" rw:"r"` + } + + r := require.New(t) + c := columns.ForStruct(withIDReadonly{}, "with_id_readonly", "id") + r.Equal(1, len(c.Cols), "%+v", c) + r.Equal(&columns.Column{Name: "id", Writeable: false, Readable: true, SelectSQL: "with_id_readonly.id"}, c.Cols["id"]) +} + +func Test_Columns_ID_Field_Not_ID(t *testing.T) { + type withNonStandardID struct { + PK string `db:"notid"` + } + + r := require.New(t) + + c := columns.ForStruct(withNonStandardID{}, "non_standard_id", "notid") + r.Equal(1, len(c.Cols), "%+v", c) + r.Equal(&columns.Column{Name: "notid", Writeable: false, Readable: true, SelectSQL: "non_standard_id.notid"}, c.Cols["notid"]) +} diff --git a/columns/readable_columns_test.go b/columns/readable_columns_test.go index 8394b967..a563d789 100644 --- a/columns/readable_columns_test.go +++ b/columns/readable_columns_test.go @@ -10,7 +10,7 @@ import ( func Test_Columns_ReadableString(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Readable().String() r.Equal(u, "LastName, first_name, read") } @@ -19,7 +19,7 @@ func Test_Columns_ReadableString(t *testing.T) { func Test_Columns_Readable_SelectString(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Readable().SelectString() r.Equal(u, "first_name as f, foo.LastName, foo.read") } @@ -28,7 +28,7 @@ func Test_Columns_Readable_SelectString(t *testing.T) { func Test_Columns_ReadableString_Symbolized(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Readable().SymbolizedString() r.Equal(u, ":LastName, :first_name, :read") } diff --git a/columns/writeable_columns_test.go b/columns/writeable_columns_test.go index 269735f3..053dbdaf 100644 --- a/columns/writeable_columns_test.go +++ b/columns/writeable_columns_test.go @@ -10,7 +10,7 @@ import ( func Test_Columns_WriteableString_Symbolized(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Writeable().SymbolizedString() r.Equal(u, ":LastName, :write") } @@ -19,7 +19,7 @@ func Test_Columns_WriteableString_Symbolized(t *testing.T) { func Test_Columns_UpdateString(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Writeable().UpdateString() r.Equal(u, "LastName = :LastName, write = :write") } @@ -35,7 +35,7 @@ func Test_Columns_QuotedUpdateString(t *testing.T) { r := require.New(t) q := testQuoter{} for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Writeable().QuotedUpdateString(q) r.Equal(u, "\"LastName\" = :LastName, \"write\" = :write") } @@ -44,7 +44,7 @@ func Test_Columns_QuotedUpdateString(t *testing.T) { func Test_Columns_WriteableString(t *testing.T) { r := require.New(t) for _, f := range []interface{}{foo{}, &foo{}} { - c := columns.ForStruct(f, "foo") + c := columns.ForStruct(f, "foo", "id") u := c.Writeable().String() r.Equal(u, "LastName, write") } diff --git a/connection.go b/connection.go index df116fb3..6e0f55ee 100644 --- a/connection.go +++ b/connection.go @@ -33,6 +33,16 @@ func (c *Connection) URL() string { return c.Dialect.URL() } +// Context returns the connection's context set by "Context()" or context.TODO() +// if no context is set. +func (c *Connection) Context() context.Context { + if c, ok := c.Store.(interface{ Context() context.Context }); ok { + return c.Context() + } + + return context.TODO() +} + // MigrationURL returns the datasource connection string used for running the migrations func (c *Connection) MigrationURL() string { return c.Dialect.MigrationURL() diff --git a/connection_details.go b/connection_details.go index 76c165b5..6456b7d1 100644 --- a/connection_details.go +++ b/connection_details.go @@ -2,13 +2,14 @@ package pop import ( "fmt" - "github.com/luna-duclos/instrumentedsql" "net/url" "regexp" "strconv" "strings" "time" + "github.com/luna-duclos/instrumentedsql" + "github.com/gobuffalo/pop/v5/internal/defaults" "github.com/gobuffalo/pop/v5/logging" "github.com/pkg/errors" @@ -179,6 +180,10 @@ func (cd *ConnectionDetails) OptionsString(s string) string { } if cd.Options != nil { for k, v := range cd.Options { + if k == "migration_table_name" { + continue + } + s = fmt.Sprintf("%s&%s=%s", s, k, v) } } diff --git a/connection_details_test.go b/connection_details_test.go index 4121745a..877c8418 100644 --- a/connection_details_test.go +++ b/connection_details_test.go @@ -84,3 +84,22 @@ func Test_ConnectionDetails_Finalize_NoDB_NoURL(t *testing.T) { err := cd.Finalize() r.Error(err) } + +func Test_ConnectionDetails_OptionsString_Postgres(t *testing.T) { + r := require.New(t) + cd := &ConnectionDetails{ + Dialect: "postgres", + Database: "database", + Host: "host", + Port: "1234", + User: "user", + Password: "pass", + Options: map[string]string{ + "migration_table_name": "migrations", + "sslmode": "require", + }, + } + + r.Equal("sslmode=require", cd.OptionsString("")) + r.Equal("migrations", cd.MigrationTableName()) +} diff --git a/connection_instrumented.go b/connection_instrumented.go index 84cf7820..f2d46b02 100644 --- a/connection_instrumented.go +++ b/connection_instrumented.go @@ -3,6 +3,7 @@ package pop import ( "database/sql" "database/sql/driver" + mysqld "github.com/go-sql-driver/mysql" "github.com/gobuffalo/pop/v5/logging" pgx "github.com/jackc/pgx/v4/stdlib" diff --git a/connection_instrumented_nosqlite_test.go b/connection_instrumented_nosqlite_test.go index 715a92ab..2cf6ae01 100644 --- a/connection_instrumented_nosqlite_test.go +++ b/connection_instrumented_nosqlite_test.go @@ -3,8 +3,9 @@ package pop import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestInstrumentation_WithoutSqlite(t *testing.T) { diff --git a/connection_instrumented_test.go b/connection_instrumented_test.go index d5125639..808d8638 100644 --- a/connection_instrumented_test.go +++ b/connection_instrumented_test.go @@ -3,12 +3,13 @@ package pop import ( "context" "fmt" - "github.com/luna-duclos/instrumentedsql" - "github.com/stretchr/testify/suite" "os" "strings" "sync" "time" + + "github.com/luna-duclos/instrumentedsql" + "github.com/stretchr/testify/suite" ) func testInstrumentedDriver(p *suite.Suite) { diff --git a/dialect_sqlite.go b/dialect_sqlite.go index b3045090..47bc3e28 100644 --- a/dialect_sqlite.go +++ b/dialect_sqlite.go @@ -5,7 +5,6 @@ package pop import ( "database/sql/driver" "fmt" - "github.com/mattn/go-sqlite3" "io" "net/url" "os" @@ -15,6 +14,8 @@ import ( "sync" "time" + "github.com/mattn/go-sqlite3" + "github.com/gobuffalo/fizz" "github.com/gobuffalo/fizz/translators" _ "github.com/mattn/go-sqlite3" // Load SQLite3 CGo driver diff --git a/executors.go b/executors.go index 9704c919..657ecea7 100644 --- a/executors.go +++ b/executors.go @@ -13,7 +13,7 @@ import ( // Reload fetch fresh data for a given model, using its ID. func (c *Connection) Reload(model interface{}) error { - sm := Model{Value: model} + sm := NewModel(model, c.Context()) return sm.iterate(func(m *Model) error { return c.Find(m.Value, m.ID()) }) @@ -51,7 +51,7 @@ func (q *Query) ExecWithCount() (int, error) { // // If model is a slice, each item of the slice is validated then saved in the database. func (c *Connection) ValidateAndSave(model interface{}, excludeColumns ...string) (*validate.Errors, error) { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) if err := sm.beforeValidate(c); err != nil { return nil, err } @@ -77,7 +77,7 @@ func IsZeroOfUnderlyingType(x interface{}) bool { // // If model is a slice, each item of the slice is saved in the database. func (c *Connection) Save(model interface{}, excludeColumns ...string) error { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) return sm.iterate(func(m *Model) error { id, err := m.fieldByName("ID") if err != nil { @@ -95,7 +95,7 @@ func (c *Connection) Save(model interface{}, excludeColumns ...string) error { // // If model is a slice, each item of the slice is validated then created in the database. func (c *Connection) ValidateAndCreate(model interface{}, excludeColumns ...string) (*validate.Errors, error) { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) if err := sm.beforeValidate(c); err != nil { return nil, err } @@ -126,7 +126,7 @@ func (c *Connection) ValidateAndCreate(model interface{}, excludeColumns ...stri continue } - sm := &Model{Value: i} + sm := NewModel(i, c.Context()) verrs, err := sm.validateAndOnlyCreate(c) if err != nil || verrs.HasAny() { return verrs, err @@ -140,14 +140,14 @@ func (c *Connection) ValidateAndCreate(model interface{}, excludeColumns ...stri continue } - sm := &Model{Value: i} + sm := NewModel(i, c.Context()) verrs, err := sm.validateAndOnlyCreate(c) if err != nil || verrs.HasAny() { return verrs, err } } - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) verrs, err = sm.validateCreate(c) if err != nil || verrs.HasAny() { return verrs, err @@ -170,7 +170,7 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error { c.disableEager() - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) return sm.iterate(func(m *Model) error { return c.timeFunc("Create", func() error { var localIsEager = isEager @@ -203,7 +203,7 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error { } if localIsEager { - sm := &Model{Value: i} + sm := NewModel(i, c.Context()) err = sm.iterate(func(m *Model) error { id, err := m.fieldByName("ID") if err != nil { @@ -228,7 +228,7 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error { } tn := m.TableName() - cols := columns.ForStructWithAlias(m.Value, tn, m.As) + cols := m.Columns() if tn == sm.TableName() { cols.Remove(excludeColumns...) @@ -255,7 +255,7 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error { continue } - sm := &Model{Value: i} + sm := NewModel(i, c.Context()) err = sm.iterate(func(m *Model) error { fbn, err := m.fieldByName("ID") if err != nil { @@ -318,7 +318,7 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error { // // If model is a slice, each item of the slice is validated then updated in the database. func (c *Connection) ValidateAndUpdate(model interface{}, excludeColumns ...string) (*validate.Errors, error) { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) if err := sm.beforeValidate(c); err != nil { return nil, err } @@ -337,7 +337,7 @@ func (c *Connection) ValidateAndUpdate(model interface{}, excludeColumns ...stri // // If model is a slice, each item of the slice is updated in the database. func (c *Connection) Update(model interface{}, excludeColumns ...string) error { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) return sm.iterate(func(m *Model) error { return c.timeFunc("Update", func() error { var err error @@ -350,8 +350,8 @@ func (c *Connection) Update(model interface{}, excludeColumns ...string) error { } tn := m.TableName() - cols := columns.ForStructWithAlias(model, tn, m.As) - cols.Remove("id", "created_at") + cols := columns.ForStructWithAlias(model, tn, m.As, m.IDField()) + cols.Remove(m.IDField(), "created_at") if tn == sm.TableName() { cols.Remove(excludeColumns...) @@ -377,7 +377,7 @@ func (c *Connection) Update(model interface{}, excludeColumns ...string) error { // // If model is a slice, each item of the slice is updated in the database. func (c *Connection) UpdateColumns(model interface{}, columnNames ...string) error { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) return sm.iterate(func(m *Model) error { return c.timeFunc("Update", func() error { var err error @@ -393,11 +393,11 @@ func (c *Connection) UpdateColumns(model interface{}, columnNames ...string) err cols := columns.Columns{} if len(columnNames) > 0 && tn == sm.TableName() { - cols = columns.NewColumnsWithAlias(tn, m.As) + cols = columns.NewColumnsWithAlias(tn, m.As, sm.IDField()) cols.Add(columnNames...) } else { - cols = columns.ForStructWithAlias(model, tn, m.As) + cols = columns.ForStructWithAlias(model, tn, m.As, m.IDField()) } cols.Remove("id", "created_at") @@ -419,7 +419,7 @@ func (c *Connection) UpdateColumns(model interface{}, columnNames ...string) err // // If model is a slice, each item of the slice is deleted from the database. func (c *Connection) Destroy(model interface{}) error { - sm := &Model{Value: model} + sm := NewModel(model, c.Context()) return sm.iterate(func(m *Model) error { return c.timeFunc("Destroy", func() error { var err error diff --git a/executors_test.go b/executors_test.go index 4fb4bb74..bee9a5e8 100644 --- a/executors_test.go +++ b/executors_test.go @@ -510,6 +510,28 @@ func Test_Create_With_Non_ID_PK_String(t *testing.T) { }) } +func Test_Create_Non_PK_ID(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + r.NoError(tx.Create(&NonStandardID{OutfacingID: "make sure the tested entry does not have pk=0"})) + + count, err := tx.Count(&NonStandardID{}) + entry := &NonStandardID{ + OutfacingID: "beautiful to the outside ID", + } + r.NoError(tx.Create(entry)) + + ctx, err := tx.Count(&NonStandardID{}) + r.NoError(err) + r.Equal(count+1, ctx) + r.NotZero(entry.ID) + }) +} + func Test_Eager_Create_Has_Many(t *testing.T) { if PDB == nil { t.Skip("skipping integration tests") @@ -1470,6 +1492,54 @@ func Test_Update_UUID(t *testing.T) { }) } +func Test_Update_With_Non_ID_PK(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + r.NoError(tx.Create(&CrookedColour{Name: "cc is not the first one"})) + + cc := CrookedColour{ + Name: "You?", + } + err := tx.Create(&cc) + r.NoError(err) + r.NotZero(cc.ID) + id := cc.ID + + updatedName := "Me!" + cc.Name = updatedName + r.NoError(tx.Update(&cc)) + r.Equal(id, cc.ID) + + r.NoError(tx.Reload(&cc)) + r.Equal(updatedName, cc.Name) + r.Equal(id, cc.ID) + }) +} + +func Test_Update_Non_PK_ID(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + r := require.New(t) + + client := &NonStandardID{ + OutfacingID: "my awesome hydra client", + } + r.NoError(tx.Create(client)) + + updatedID := "your awesome hydra client" + client.OutfacingID = updatedID + r.NoError(tx.Update(client)) + r.NoError(tx.Reload(client)) + r.Equal(updatedID, client.OutfacingID) + }) +} + func Test_Destroy(t *testing.T) { if PDB == nil { t.Skip("skipping integration tests") diff --git a/finders.go b/finders.go index 9026d14b..fa1a220a 100644 --- a/finders.go +++ b/finders.go @@ -29,7 +29,7 @@ func (c *Connection) Find(model interface{}, id interface{}) error { // // q.Find(&User{}, 1) func (q *Query) Find(model interface{}, id interface{}) error { - m := &Model{Value: model} + m := NewModel(model, q.Connection.Context()) idq := m.whereID() switch t := id.(type) { case uuid.UUID: @@ -69,7 +69,7 @@ func (c *Connection) First(model interface{}) error { func (q *Query) First(model interface{}) error { err := q.Connection.timeFunc("First", func() error { q.Limit(1) - m := &Model{Value: model} + m := NewModel(model, q.Connection.Context()) if err := q.Connection.Dialect.SelectOne(q.Connection.Store, m, *q); err != nil { return err } @@ -102,7 +102,7 @@ func (q *Query) Last(model interface{}) error { err := q.Connection.timeFunc("Last", func() error { q.Limit(1) q.Order("created_at DESC, id DESC") - m := &Model{Value: model} + m := NewModel(model, q.Connection.Context()) if err := q.Connection.Dialect.SelectOne(q.Connection.Store, m, *q); err != nil { return err } @@ -134,7 +134,7 @@ func (c *Connection) All(models interface{}) error { // q.Where("name = ?", "mark").All(&[]User{}) func (q *Query) All(models interface{}) error { err := q.Connection.timeFunc("All", func() error { - m := &Model{Value: models} + m := NewModel(models, q.Connection.Context()) err := q.Connection.Dialect.SelectMany(q.Connection.Store, m, *q) if err != nil { return err @@ -258,7 +258,7 @@ func (q *Query) eagerDefaultAssociations(model interface{}) error { } } - sqlSentence, args := query.ToSQL(&Model{Value: association.Interface()}) + sqlSentence, args := query.ToSQL(NewModel(association.Interface(), query.Connection.Context())) query = query.RawQuery(sqlSentence, args...) if association.Kind() == reflect.Slice || association.Kind() == reflect.Array { @@ -302,7 +302,7 @@ func (q *Query) Exists(model interface{}) (bool, error) { tmpQuery.Paginator = nil tmpQuery.orderClauses = clauses{} tmpQuery.limitResults = 0 - query, args := tmpQuery.ToSQL(&Model{Value: model}) + query, args := tmpQuery.ToSQL(NewModel(model, tmpQuery.Connection.Context())) // when query contains custom selected fields / executed using RawQuery, // sql may already contains limit and offset @@ -348,7 +348,7 @@ func (q Query) CountByField(model interface{}, field string) (int, error) { tmpQuery.Paginator = nil tmpQuery.orderClauses = clauses{} tmpQuery.limitResults = 0 - query, args := tmpQuery.ToSQL(&Model{Value: model}) + query, args := tmpQuery.ToSQL(NewModel(model, q.Connection.Context())) // when query contains custom selected fields / executed using RawQuery, // sql may already contains limit and offset diff --git a/finders_test.go b/finders_test.go index 389dc80a..7f30727e 100644 --- a/finders_test.go +++ b/finders_test.go @@ -101,7 +101,7 @@ func Test_Select(t *testing.T) { q := tx.Select("name", "email", "\n", "\t\n", "") - sm := &Model{Value: &User{}} + sm := NewModel(new(User), tx.Context()) sql, _ := q.ToSQL(sm) r.Equal(tx.Dialect.TranslateSQL("SELECT email, name FROM users AS users"), sql) diff --git a/go.mod b/go.mod index 225a6ac6..346ac0ee 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,8 @@ require ( github.com/gobuffalo/plush/v4 v4.0.0 github.com/gobuffalo/validate/v3 v3.1.0 github.com/gofrs/uuid v3.2.0+incompatible - github.com/jackc/pgconn v1.6.0 - github.com/jackc/pgx/v4 v4.6.0 + github.com/jackc/pgconn v1.8.0 + github.com/jackc/pgx/v4 v4.10.1 github.com/jmoiron/sqlx v1.2.0 github.com/karrick/godirwalk v1.16.1 // indirect github.com/lib/pq v1.3.0 diff --git a/go.sum b/go.sum index 22100ac4..e00ce20b 100644 --- a/go.sum +++ b/go.sum @@ -47,7 +47,6 @@ github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6 github.com/gobuffalo/fizz v1.10.0 h1:I8vad0PnmR+CLjSnZ5L5jlhBm4S88UIGOoZZL3/3e24= github.com/gobuffalo/fizz v1.10.0/go.mod h1:J2XGPO0AfJ1zKw7+2BA+6FEGAkyEsdCOLvN93WCT2WI= github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/flect v0.2.0 h1:EWCvMGGxOjsgwlWaP+f4+Hh6yrrte7JeFL2S6b+0hdM= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.2.1 h1:GPoRjEN0QObosV4XwuoWvSd5uSiL0N3e91/xqyY4crQ= github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= @@ -55,7 +54,6 @@ github.com/gobuffalo/genny/v2 v2.0.5 h1:IH0EHcvwKT0MdASzptvkz/ViYBQELTklq1/l8Ot3 github.com/gobuffalo/genny/v2 v2.0.5/go.mod h1:kRkJuAw9mdI37AiEYjV4Dl+TgkBDYf8HZVjLkqe5eBg= github.com/gobuffalo/github_flavored_markdown v1.1.0 h1:8Zzj4fTRl/OP2R7sGerzSf6g2nEJnaBEJe7UAOiEvbQ= github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic= -github.com/gobuffalo/helpers v0.6.0 h1:CL1xOSGeKCaKD1IUpo4RfrkDU83kmkMG4H3dXAS7dw0= github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8= github.com/gobuffalo/helpers v0.6.1 h1:LLcL4BsiyDQYtMRUUpyFdBFvFXQ6hNYOpwrcYeilVWM= github.com/gobuffalo/helpers v0.6.1/go.mod h1:wInbDi0vTJKZBviURTLRMFLE4+nF2uRuuL2fnlYo7w4= @@ -63,7 +61,6 @@ github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gq github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= github.com/gobuffalo/nulls v0.2.0 h1:7R0Uec6JlZI02TR29zrs3KFIuUV8Sqe/s/j3yLvs+gc= github.com/gobuffalo/nulls v0.2.0/go.mod h1:w4q8RoSCEt87Q0K0sRIZWYeIxkxog5mh3eN3C/n+dUc= -github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= @@ -71,11 +68,9 @@ github.com/gobuffalo/packr/v2 v2.8.0 h1:IULGd15bQL59ijXLxEvA5wlMxsmx/ZkQv9T282zN github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g= github.com/gobuffalo/plush/v4 v4.0.0 h1:ZHdmfr2R7DQ77XzWZK2PGKJOXm9NRy21EZ6Rw7FhuNw= github.com/gobuffalo/plush/v4 v4.0.0/go.mod h1:ErFS3UxKqEb8fpFJT7lYErfN/Nw6vHGiDMTjxpk5bQ0= -github.com/gobuffalo/tags/v3 v3.0.2 h1:gxE6c6fA5radwQeg59aPIeYgCG8YA8AZd3Oh6fh5UXA= github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= github.com/gobuffalo/tags/v3 v3.1.0 h1:mzdCYooN2VsLRr8KIAdEZ1lh1Py7JSMsiEGCGata2AQ= github.com/gobuffalo/tags/v3 v3.1.0/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= -github.com/gobuffalo/validate/v3 v3.0.0 h1:dF7Bg8NMF9Zv8bZvUMXYJXxZdj+eSZ8z/lGM7/jVFUE= github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= github.com/gobuffalo/validate/v3 v3.1.0 h1:/QQN920PciCfBs3aywtJTvDTHmBFMKoiwkshUWa/HLQ= github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= @@ -90,6 +85,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -99,16 +95,17 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0 h1:DUwgMQuuPnS0rhMXenUtZpqZqrR/30NWY+qQvTpSvEs= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.6.0 h1:8FiBxMxS/Z0eQ9BeE1HhL6pzPL1R5x+ZuQ+T86WgZ4I= -github.com/jackc/pgconn v1.6.0/go.mod h1:yeseQo4xhQbgyJs2c87RAXOH2i624N0Fh1KSPJya7qo= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= @@ -121,36 +118,39 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1 h1:Rdjp4NFjwHnEslx2b66FfCI2S0LhO4itac3hXz6WX9M= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY= -github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8 h1:Q3tB+ExeflWUW7AFcAhXqk40s9mnNYLk1nOkKNZ5GnU= +github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59 h1:xOamcCJ9MFJTxR5bvw3ZXmiP8evQMohdt2VJ57C0W8Q= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.3.0 h1:l8JvKrby3RI7Kg3bYEeU9TA4vqC38QDpFCfcrC7KuN0= -github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8= +github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186 h1:ZQM8qLT/E/CGD6XX0E6q9FAwxJYmWpJufzmLMaFuzgQ= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.6.0 h1:Fh0O9GdlG4gYpjpwOqjdEodJUQM9jzN3Hdv7PN0xmm0= -github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY= +github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/karrick/godirwalk v1.15.3 h1:0a2pXOgtB16CqIqXTiT7+K9L73f74n/aNQUnH6Ortew= github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= @@ -162,7 +162,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -186,14 +185,16 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -220,6 +221,7 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -227,14 +229,13 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -259,7 +260,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -271,20 +271,27 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 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/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -296,9 +303,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -322,29 +327,32 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9 h1:KOkk4e2xd5OeCDJGwacvr75ICCbCsShrHiqPEdsA9hg= golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200308013534-11ec41452d41 h1:9Di9iYgOt9ThCipBxChBVhgNipDoE5mxO84rQV7D0FE= golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -354,7 +362,6 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -368,3 +375,4 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/match_test.go b/match_test.go index 8cc41a6b..0f1591f1 100644 --- a/match_test.go +++ b/match_test.go @@ -1,8 +1,9 @@ package pop import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func Test_ParseMigrationFilenameFizzDown(t *testing.T) { diff --git a/migration_info_test.go b/migration_info_test.go index 0f76824e..f2174551 100644 --- a/migration_info_test.go +++ b/migration_info_test.go @@ -1,9 +1,10 @@ package pop import ( - "github.com/stretchr/testify/assert" "sort" "testing" + + "github.com/stretchr/testify/assert" ) func TestSortingMigrations(t *testing.T) { diff --git a/model.go b/model.go index 62efd25b..1dea97f5 100644 --- a/model.go +++ b/model.go @@ -1,22 +1,23 @@ package pop import ( + "context" "fmt" - "github.com/pkg/errors" "reflect" - "sync" + "strings" "time" - "github.com/gobuffalo/flect" nflect "github.com/gobuffalo/flect/name" + + "github.com/gobuffalo/pop/v5/columns" + "github.com/pkg/errors" + + "github.com/gobuffalo/flect" "github.com/gofrs/uuid" ) var nowFunc = time.Now -var tableMap = map[string]string{} -var tableMapMu = sync.RWMutex{} - // Value is the contents of a `Model`. type Value interface{} @@ -26,8 +27,13 @@ type modelIterable func(*Model) error // that is passed in to many functions. type Model struct { Value - tableName string - As string + ctx context.Context + As string +} + +// NewModel returns a new model with the specified value and context. +func NewModel(v Value, ctx context.Context) *Model { + return &Model{Value: v, ctx: ctx} } // ID returns the ID of the Model. All models must have an `ID` field this is @@ -46,7 +52,18 @@ func (m *Model) ID() interface{} { // IDField returns the name of the DB field used for the ID. // By default, it will return "id". func (m *Model) IDField() string { - field, ok := reflect.TypeOf(m.Value).Elem().FieldByName("ID") + modelType := reflect.TypeOf(m.Value) + + // remove all indirections + for modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Array { + modelType = modelType.Elem() + } + + if modelType.Kind() == reflect.String { + return "id" + } + + field, ok := modelType.FieldByName("ID") if !ok { return "id" } @@ -74,38 +91,43 @@ type TableNameAble interface { TableName() string } +// TableNameAbleWithContext is equal to TableNameAble but will +// be passed the queries' context. Useful in cases where the +// table name depends on e.g. +type TableNameAbleWithContext interface { + TableName(ctx context.Context) string +} + // TableName returns the corresponding name of the underlying database table // for a given `Model`. See also `TableNameAble` to change the default name of the table. func (m *Model) TableName() string { if s, ok := m.Value.(string); ok { return s } + if n, ok := m.Value.(TableNameAble); ok { return n.TableName() } - if m.tableName != "" { - return m.tableName + if n, ok := m.Value.(TableNameAbleWithContext); ok { + if m.ctx == nil { + m.ctx = context.TODO() + } + return n.TableName(m.ctx) } - t := reflect.TypeOf(m.Value) - name, cacheKey := m.typeName(t) - - defer tableMapMu.Unlock() - tableMapMu.Lock() + return m.typeName(reflect.TypeOf(m.Value)) +} - if tableMap[cacheKey] == "" { - m.tableName = nflect.Tableize(name) - tableMap[cacheKey] = m.tableName - } - return tableMap[cacheKey] +func (m *Model) Columns() columns.Columns { + return columns.ForStructWithAlias(m.Value, m.TableName(), m.As, m.IDField()) } func (m *Model) cacheKey(t reflect.Type) string { return t.PkgPath() + "." + t.Name() } -func (m *Model) typeName(t reflect.Type) (name, cacheKey string) { +func (m *Model) typeName(t reflect.Type) (name string) { if t.Kind() == reflect.Ptr { t = t.Elem() } @@ -117,19 +139,27 @@ func (m *Model) typeName(t reflect.Type) (name, cacheKey string) { } // validates if the elem of slice or array implements TableNameAble interface. - tableNameAble := (*TableNameAble)(nil) + var tableNameAble *TableNameAble if el.Implements(reflect.TypeOf(tableNameAble).Elem()) { v := reflect.New(el) out := v.MethodByName("TableName").Call([]reflect.Value{}) - name := out[0].String() - if tableMap[m.cacheKey(el)] == "" { - tableMap[m.cacheKey(el)] = name - } + return out[0].String() + } + + // validates if the elem of slice or array implements TableNameAbleWithContext interface. + var tableNameAbleWithContext *TableNameAbleWithContext + if el.Implements(reflect.TypeOf(tableNameAbleWithContext).Elem()) { + v := reflect.New(el) + out := v.MethodByName("TableName").Call([]reflect.Value{reflect.ValueOf(m.ctx)}) + return out[0].String() + + // We do not want to cache contextualized TableNames because that would break + // the contextualization. } - return el.Name(), m.cacheKey(el) + return nflect.Tableize(el.Name()) default: - return t.Name(), m.cacheKey(t) + return nflect.Tableize(t.Name()) } } @@ -193,11 +223,21 @@ func (m *Model) touchUpdatedAt() { } func (m *Model) whereID() string { - return fmt.Sprintf("%s.%s = ?", m.TableName(), m.IDField()) + as := m.As + if as == "" { + as = strings.ReplaceAll(m.TableName(), ".", "_") + } + + return fmt.Sprintf("%s.%s = ?", as, m.IDField()) } func (m *Model) whereNamedID() string { - return fmt.Sprintf("%s.%s = :%s", m.TableName(), m.IDField(), m.IDField()) + as := m.As + if as == "" { + as = strings.ReplaceAll(m.TableName(), ".", "_") + } + + return fmt.Sprintf("%s.%s = :%s", as, m.IDField(), m.IDField()) } func (m *Model) isSlice() bool { @@ -210,7 +250,10 @@ func (m *Model) iterate(fn modelIterable) error { v := reflect.Indirect(reflect.ValueOf(m.Value)) for i := 0; i < v.Len(); i++ { val := v.Index(i) - newModel := &Model{Value: val.Addr().Interface()} + newModel := &Model{ + Value: val.Addr().Interface(), + ctx: m.ctx, + } err := fn(newModel) if err != nil { diff --git a/model_context_test.go b/model_context_test.go new file mode 100644 index 00000000..6b03a882 --- /dev/null +++ b/model_context_test.go @@ -0,0 +1,109 @@ +package pop + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type ContextTable struct { + ID string `db:"id"` + Value string `db:"value"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (t ContextTable) TableName(ctx context.Context) string { + // This is singular on purpose! It will checck if the TableName is properly + // Respected in slices as well. + return "context_prefix_" + ctx.Value("prefix").(string) + "_table" +} + +func Test_ModelContext(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + + t.Run("contextless", func(t *testing.T) { + r := require.New(t) + r.Panics(func() { + var c ContextTable + r.NoError(PDB.Create(&c)) + }, "panics if context prefix is not set") + }) + + for _, prefix := range []string{"a", "b"} { + t.Run("prefix="+prefix, func(t *testing.T) { + r := require.New(t) + + expected := ContextTable{ID: prefix, Value: prefix} + c := PDB.WithContext(context.WithValue(context.Background(), "prefix", prefix)) + r.NoError(c.Create(&expected)) + + var actual ContextTable + r.NoError(c.Find(&actual, expected.ID)) + r.EqualValues(prefix, actual.Value) + r.EqualValues(prefix, actual.ID) + + exists, err := c.Where("id = ?", actual.ID).Exists(new(ContextTable)) + r.NoError(err) + r.True(exists) + + count, err := c.Where("id = ?", actual.ID).Count(new(ContextTable)) + r.NoError(err) + r.EqualValues(1, count) + + expected.Value += expected.Value + r.NoError(c.Update(&expected)) + + r.NoError(c.Find(&actual, expected.ID)) + r.EqualValues(prefix+prefix, actual.Value) + r.EqualValues(prefix, actual.ID) + + var results []ContextTable + require.NoError(t, c.All(&results)) + + require.NoError(t, c.First(&expected)) + require.NoError(t, c.Last(&expected)) + + r.NoError(c.Destroy(&expected)) + }) + } + + t.Run("prefix=unknown", func(t *testing.T) { + r := require.New(t) + c := PDB.WithContext(context.WithValue(context.Background(), "prefix", "unknown")) + err := c.Create(&ContextTable{ID: "unknown"}) + r.Error(err) + + if !strings.Contains(err.Error(), "context_prefix_unknown_table") { // All other databases + t.Fatalf("Expected error to contain indicator that table does not exist but got: %s", err.Error()) + } + }) + + t.Run("cache_busting", func(t *testing.T) { + r := require.New(t) + + var expectedA, expectedB ContextTable + expectedA.ID = "expectedA" + expectedB.ID = "expectedB" + + cA := PDB.WithContext(context.WithValue(context.Background(), "prefix", "a")) + r.NoError(cA.Create(&expectedA)) + + cB := PDB.WithContext(context.WithValue(context.Background(), "prefix", "b")) + r.NoError(cB.Create(&expectedB)) + + var actualA, actualB []ContextTable + r.NoError(cA.All(&actualA)) + r.NoError(cB.All(&actualB)) + + r.Len(actualA, 1) + r.Len(actualB, 1) + + r.NotEqual(actualA[0].ID, actualB[0].ID, "if these are equal context switching did not work") + }) +} diff --git a/model_test.go b/model_test.go index 545eb09e..d0d0c23c 100644 --- a/model_test.go +++ b/model_test.go @@ -1,9 +1,14 @@ package pop import ( + "context" + "fmt" "testing" "time" + "github.com/gobuffalo/pop/v5/testdata/models/ac" + "github.com/gobuffalo/pop/v5/testdata/models/bc" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,26 +17,25 @@ import ( ) func Test_Model_TableName(t *testing.T) { - r := require.New(t) - - m := Model{Value: User{}} - r.Equal(m.TableName(), "users") - - m = Model{Value: &User{}} - r.Equal(m.TableName(), "users") - - m = Model{Value: &Users{}} - r.Equal(m.TableName(), "users") - - m = Model{Value: []User{}} - r.Equal(m.TableName(), "users") - - m = Model{Value: &[]User{}} - r.Equal(m.TableName(), "users") - - m = Model{Value: []*User{}} - r.Equal(m.TableName(), "users") - + for k, v := range []interface{}{ + User{}, + &User{}, + + &Users{}, + Users{}, + + []*User{}, + &[]*User{}, + + []User{}, + &[]User{}, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + r := require.New(t) + m := Model{Value: v} + r.Equal("users", m.TableName()) + }) + } } type tn struct{} @@ -40,6 +44,12 @@ func (tn) TableName() string { return "this is my table name" } +type tnc struct{} + +func (tnc) TableName(ctx context.Context) string { + return ctx.Value("name").(string) +} + // A failing test case for #477 func Test_TableNameCache(t *testing.T) { r := assert.New(t) @@ -49,12 +59,27 @@ func Test_TableNameCache(t *testing.T) { r.Equal("userb", (&Model{Value: []b.User{}}).TableName()) } +// A failing test case for #477 +func Test_TableNameContextCache(t *testing.T) { + ctx := context.WithValue(context.Background(), "name", "context_table") + + r := assert.New(t) + r.Equal("context_table_useras", (&Model{Value: ac.User{}, ctx: ctx}).TableName()) + r.Equal("context_table_userbs", (&Model{Value: bc.User{}, ctx: ctx}).TableName()) + r.Equal("context_table_useras", (&Model{Value: []ac.User{}, ctx: ctx}).TableName()) + r.Equal("context_table_userbs", (&Model{Value: []bc.User{}, ctx: ctx}).TableName()) +} + func Test_TableName(t *testing.T) { r := require.New(t) cases := []interface{}{ tn{}, + &tn{}, []tn{}, + &[]tn{}, + []*tn{}, + &[]*tn{}, } for _, tc := range cases { m := Model{Value: tc} @@ -62,6 +87,22 @@ func Test_TableName(t *testing.T) { } } +func Test_TableNameContext(t *testing.T) { + r := require.New(t) + + tn := "context_table_names" + ctx := context.WithValue(context.Background(), "name", tn) + + cases := []interface{}{ + tnc{}, + []tnc{}, + } + for _, tc := range cases { + m := Model{Value: tc, ctx: ctx} + r.Equal(tn, m.TableName()) + } +} + type TimeTimestamp struct { ID int `db:"id"` CreatedAt time.Time `db:"created_at"` @@ -77,7 +118,7 @@ type UnixTimestamp struct { func Test_Touch_Time_Timestamp(t *testing.T) { r := require.New(t) - m := Model{Value: &TimeTimestamp{}} + m := NewModel(&TimeTimestamp{}, context.Background()) // Override time.Now() t0, _ := time.Parse(time.RFC3339, "2019-07-14T00:00:00Z") @@ -101,7 +142,7 @@ func Test_Touch_Time_Timestamp_With_Existing_Value(t *testing.T) { createdAt := nowFunc().Add(-36 * time.Hour) - m := Model{Value: &TimeTimestamp{CreatedAt: createdAt}} + m := NewModel(&TimeTimestamp{CreatedAt: createdAt}, context.Background()) m.touchCreatedAt() m.touchUpdatedAt() v := m.Value.(*TimeTimestamp) @@ -112,7 +153,7 @@ func Test_Touch_Time_Timestamp_With_Existing_Value(t *testing.T) { func Test_Touch_Unix_Timestamp(t *testing.T) { r := require.New(t) - m := Model{Value: &UnixTimestamp{}} + m := NewModel(&UnixTimestamp{}, context.Background()) // Override time.Now() t0, _ := time.Parse(time.RFC3339, "2019-07-14T00:00:00Z") @@ -136,7 +177,7 @@ func Test_Touch_Unix_Timestamp_With_Existing_Value(t *testing.T) { createdAt := int(time.Now().Add(-36 * time.Hour).Unix()) - m := Model{Value: &UnixTimestamp{CreatedAt: createdAt}} + m := NewModel(&UnixTimestamp{CreatedAt: createdAt}, context.Background()) m.touchCreatedAt() m.touchUpdatedAt() v := m.Value.(*UnixTimestamp) @@ -159,3 +200,25 @@ func Test_IDField(t *testing.T) { m = Model{Value: &testNormalID{ID: 1}} r.Equal("id", m.IDField()) } + +type testPrefixID struct { + ID int `db:"custom_id"` +} + +func (t testPrefixID) TableName() string { + return "foo.bar" +} + +func Test_WhereID(t *testing.T) { + r := require.New(t) + m := Model{Value: &testPrefixID{ID: 1}} + + r.Equal("foo_bar.custom_id = ?", m.whereID()) + r.Equal("foo_bar.custom_id = :custom_id", m.whereNamedID()) + + type testNormalID struct { + ID int + } + m = Model{Value: &testNormalID{ID: 1}} + r.Equal("id", m.IDField()) +} diff --git a/pop_test.go b/pop_test.go index 33efcbe2..1d473de0 100644 --- a/pop_test.go +++ b/pop_test.go @@ -138,14 +138,16 @@ type Book struct { } type Taxi struct { - ID int `db:"id"` - Model string `db:"model"` - UserID nulls.Int `db:"user_id"` - AddressID nulls.Int `db:"address_id"` - Driver *User `belongs_to:"user" fk_id:"user_id"` - Address Address `belongs_to:"address"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID int `db:"id"` + Model string `db:"model"` + UserID nulls.Int `db:"user_id"` + AddressID nulls.Int `db:"address_id"` + Driver *User `belongs_to:"user" fk_id:"user_id"` + Address Address `belongs_to:"address"` + ToAddressID *int `db:"to_address_id"` + ToAddress *Address `belongs_to:"address"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } // Validate gets run every time you call a "Validate*" (ValidateAndSave, ValidateAndCreate, ValidateAndUpdate) method. @@ -419,3 +421,8 @@ type CrookedSong struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type NonStandardID struct { + ID int `db:"pk"` + OutfacingID string `db:"id"` +} diff --git a/preload_associations.go b/preload_associations.go index 4a03b579..c7c8b6c5 100644 --- a/preload_associations.go +++ b/preload_associations.go @@ -167,7 +167,7 @@ func (ami *AssociationMetaInfo) fkName() string { // preload is the query mode used to load associations from database // similar to the active record default approach on Rails. func preload(tx *Connection, model interface{}, fields ...string) error { - mmi := NewModelMetaInfo(&Model{Value: model}) + mmi := NewModelMetaInfo(NewModel(model, tx.Context())) preloadFields, err := mmi.preloadFields(fields...) if err != nil { @@ -355,7 +355,9 @@ func preloadBelongsTo(tx *Connection, asoc *AssociationMetaInfo, mmi *ModelMetaI fkids := []interface{}{} mmi.iterate(func(val reflect.Value) { - fkids = append(fkids, mmi.mapper.FieldByName(val, fi.Path).Interface()) + if !isFieldNilPtr(val, fi) { + fkids = append(fkids, mmi.mapper.FieldByName(val, fi.Path).Interface()) + } }) if len(fkids) == 0 { @@ -386,11 +388,15 @@ func preloadBelongsTo(tx *Connection, asoc *AssociationMetaInfo, mmi *ModelMetaI // 3) iterate over every model and fill it with the assoc. mmi.iterate(func(mvalue reflect.Value) { + if isFieldNilPtr(mvalue, fi) { + return + } modelAssociationField := mmi.mapper.FieldByName(mvalue, asoc.Name) for i := 0; i < slice.Elem().Len(); i++ { asocValue := slice.Elem().Index(i) - if mmi.mapper.FieldByName(mvalue, fi.Path).Interface() == mmi.mapper.FieldByName(asocValue, "ID").Interface() || - reflect.DeepEqual(mmi.mapper.FieldByName(mvalue, fi.Path), mmi.mapper.FieldByName(asocValue, "ID")) { + fkField := reflect.Indirect(mmi.mapper.FieldByName(mvalue, fi.Path)) + if fkField.Interface() == mmi.mapper.FieldByName(asocValue, "ID").Interface() || + reflect.DeepEqual(fkField, mmi.mapper.FieldByName(asocValue, "ID")) { switch { case modelAssociationField.Kind() == reflect.Slice || modelAssociationField.Kind() == reflect.Array: @@ -499,3 +505,8 @@ func preloadManyToMany(tx *Connection, asoc *AssociationMetaInfo, mmi *ModelMeta } return nil } + +func isFieldNilPtr(val reflect.Value, fi *reflectx.FieldInfo) bool { + fieldValue := reflectx.FieldByIndexesReadOnly(val, fi.Index) + return fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() +} diff --git a/preload_associations_test.go b/preload_associations_test.go index af1f392f..9d735fd4 100644 --- a/preload_associations_test.go +++ b/preload_associations_test.go @@ -231,3 +231,36 @@ func Test_New_Implementation_For_BelongsTo_Multiple_Fields(t *testing.T) { SetEagerMode(EagerDefault) }) } + +func Test_New_Implementation_For_BelongsTo_Ptr_Field(t *testing.T) { + if PDB == nil { + t.Skip("skipping integration tests") + } + transaction(func(tx *Connection) { + a := require.New(t) + toAddress := Address{HouseNumber: 1, Street: "Destination Ave"} + a.NoError(tx.Create(&toAddress)) + + taxi := Taxi{ToAddressID: &toAddress.ID} + a.NoError(tx.Create(&taxi)) + + book1 := Book{TaxiID: nulls.NewInt(taxi.ID), Title: "My Book"} + a.NoError(tx.Create(&book1)) + + taxiNilToAddress := Taxi{ToAddressID: nil} + a.NoError(tx.Create(&taxiNilToAddress)) + + book2 := Book{TaxiID: nulls.NewInt(taxiNilToAddress.ID), Title: "Another Book"} + a.NoError(tx.Create(&book2)) + + SetEagerMode(EagerPreload) + books := []Book{} + a.NoError(tx.EagerPreload("Taxi.ToAddress").Order("created_at").All(&books)) + a.Len(books, 2) + a.Equal(toAddress.Street, books[0].Taxi.ToAddress.Street) + a.NotNil(books[0].Taxi.ToAddressID) + a.Nil(books[1].Taxi.ToAddress) + a.Nil(books[1].Taxi.ToAddressID) + SetEagerMode(EagerDefault) + }) +} diff --git a/query_test.go b/query_test.go index 93da765c..7df5a994 100644 --- a/query_test.go +++ b/query_test.go @@ -1,6 +1,7 @@ package pop import ( + "context" "fmt" "testing" @@ -13,7 +14,7 @@ func Test_Where(t *testing.T) { t.Skip("skipping integration tests") } a := require.New(t) - m := &Model{Value: &Enemy{}} + m := NewModel(new(Enemy), context.Background()) q := PDB.Where("id = ?", 1) sql, _ := q.ToSQL(m) @@ -107,7 +108,7 @@ func Test_Order(t *testing.T) { } a := require.New(t) - m := &Model{Value: &Enemy{}} + m := NewModel(&Enemy{}, context.Background()) q := PDB.Order("id desc") sql, _ := q.ToSQL(m) a.Equal(ts("SELECT enemies.A FROM enemies AS enemies ORDER BY id desc"), sql) @@ -123,7 +124,7 @@ func Test_GroupBy(t *testing.T) { } a := require.New(t) - m := &Model{Value: &Enemy{}} + m := NewModel(&Enemy{}, context.Background()) q := PDB.Q() q.GroupBy("A") sql, _ := q.ToSQL(m) @@ -159,7 +160,7 @@ func Test_ToSQL(t *testing.T) { } a := require.New(t) transaction(func(tx *Connection) { - user := &Model{Value: &User{}} + user := NewModel(&User{}, tx.Context()) s := "SELECT name as full_name, users.alive, users.bio, users.birth_date, users.created_at, users.email, users.id, users.name, users.price, users.updated_at, users.user_name FROM users AS users" @@ -171,10 +172,10 @@ func Test_ToSQL(t *testing.T) { q, _ = query.ToSQL(user) a.Equal(fmt.Sprintf("%s ORDER BY id desc", s), q) - q, _ = query.ToSQL(&Model{Value: &User{}, As: "u"}) + q, _ = query.ToSQL(&Model{Value: &User{}, As: "u", ctx: tx.Context()}) a.Equal("SELECT name as full_name, u.alive, u.bio, u.birth_date, u.created_at, u.email, u.id, u.name, u.price, u.updated_at, u.user_name FROM users AS u ORDER BY id desc", q) - q, _ = query.ToSQL(&Model{Value: &Family{}}) + q, _ = query.ToSQL(&Model{Value: &Family{}, ctx: tx.Context()}) a.Equal("SELECT family_members.created_at, family_members.first_name, family_members.id, family_members.last_name, family_members.updated_at FROM family.members AS family_members ORDER BY id desc", q) query = tx.Where("id = 1") @@ -262,7 +263,7 @@ func Test_ToSQLInjection(t *testing.T) { } a := require.New(t) transaction(func(tx *Connection) { - user := &Model{Value: &User{}} + user := NewModel(new(User), tx.Context()) query := tx.Where("name = '?'", "\\\u0027 or 1=1 limit 1;\n-- ") q, _ := query.ToSQL(user) a.NotEqual("SELECT * FROM users AS users WHERE name = '\\'' or 1=1 limit 1;\n-- '", q) diff --git a/scopes_test.go b/scopes_test.go index 0e22b76e..f393ffb3 100644 --- a/scopes_test.go +++ b/scopes_test.go @@ -1,6 +1,7 @@ package pop import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -13,7 +14,7 @@ func Test_Scopes(t *testing.T) { r := require.New(t) oql := "SELECT enemies.A FROM enemies AS enemies" - m := &Model{Value: &Enemy{}} + m := NewModel(new(Enemy), context.Background()) q := PDB.Q() s, _ := q.ToSQL(m) diff --git a/slices/map.go b/slices/map.go index d7f58ed4..c99e8a13 100644 --- a/slices/map.go +++ b/slices/map.go @@ -51,6 +51,9 @@ func (m Map) UnmarshalJSON(b []byte) error { if err != nil { return err } + if m == nil { + m = Map{} + } for key, value := range stuff { m[key] = value } diff --git a/slices/map_test.go b/slices/map_test.go index e5d4963b..265d9940 100644 --- a/slices/map_test.go +++ b/slices/map_test.go @@ -24,3 +24,14 @@ func Test_Map_MarshalJSON(t *testing.T) { r.NoError(err) r.Equal([]byte(`{"a":"b"}`), b) } + +func Test_Map_UnMarshalJSON_uninitialized_map_does_not_panic(t *testing.T) { + r := require.New(t) + + maps := make([]Map, 0) + r.NotPanics(func() { + err := json.Unmarshal([]byte(`[{"a": "b"}]`), &maps) + r.NoError(err) + r.Len(maps, 1) + }) +} diff --git a/soda/cmd/migrate_status.go b/soda/cmd/migrate_status.go index e9a85df6..98d0aae4 100644 --- a/soda/cmd/migrate_status.go +++ b/soda/cmd/migrate_status.go @@ -1,9 +1,10 @@ package cmd import ( + "os" + "github.com/gobuffalo/pop/v5" "github.com/spf13/cobra" - "os" ) var migrateStatusCmd = &cobra.Command{ diff --git a/sql_builder.go b/sql_builder.go index 4ce35703..edd05c46 100644 --- a/sql_builder.go +++ b/sql_builder.go @@ -229,7 +229,7 @@ func (sq *sqlBuilder) buildColumns() columns.Columns { if ok && cols.TableAlias == asName { return cols } - cols = columns.ForStructWithAlias(sq.Model.Value, tableName, asName) + cols = columns.ForStructWithAlias(sq.Model.Value, tableName, asName, sq.Model.IDField()) columnCacheMutex.Lock() columnCache[tableName] = cols columnCacheMutex.Unlock() @@ -237,7 +237,7 @@ func (sq *sqlBuilder) buildColumns() columns.Columns { } // acl > 0 - cols := columns.NewColumns("") + cols := columns.NewColumns("", sq.Model.IDField()) cols.Add(sq.AddColumns...) return cols } diff --git a/store.go b/store.go index cc98a266..39c554ed 100644 --- a/store.go +++ b/store.go @@ -54,3 +54,7 @@ func (s contextStore) Exec(query string, args ...interface{}) (sql.Result, error func (s contextStore) PrepareNamed(query string) (*sqlx.NamedStmt, error) { return s.store.PrepareNamedContext(s.ctx, query) } + +func (s contextStore) Context() context.Context { + return s.ctx +} diff --git a/testdata/migrations/20181104135856_taxis.up.fizz b/testdata/migrations/20181104135856_taxis.up.fizz index 9cf85238..a11185c0 100644 --- a/testdata/migrations/20181104135856_taxis.up.fizz +++ b/testdata/migrations/20181104135856_taxis.up.fizz @@ -2,6 +2,7 @@ create_table("taxis") { t.Column("id", "int", {primary: true}) t.Column("model", "string", {}) t.Column("user_id", "int", {"null": true}) - t.Column("address_id", "int",{"null":true}) + t.Column("address_id", "int", {"null":true}) + t.Column("to_address_id", "int", {"null":true}) t.Timestamps() } \ No newline at end of file diff --git a/testdata/migrations/20201028153041_non_standard_id.down.fizz b/testdata/migrations/20201028153041_non_standard_id.down.fizz new file mode 100644 index 00000000..5c56284f --- /dev/null +++ b/testdata/migrations/20201028153041_non_standard_id.down.fizz @@ -0,0 +1 @@ +drop_table("non_standard_ids") diff --git a/testdata/migrations/20201028153041_non_standard_id.up.fizz b/testdata/migrations/20201028153041_non_standard_id.up.fizz new file mode 100644 index 00000000..7590e4b4 --- /dev/null +++ b/testdata/migrations/20201028153041_non_standard_id.up.fizz @@ -0,0 +1,6 @@ +create_table("non_standard_ids") { + t.Column("pk", "int", { primary: true }) + t.Column("id", "string", {}) + + t.DisableTimestamps() +} diff --git a/testdata/migrations/20210104145901_context_tables.down.fizz b/testdata/migrations/20210104145901_context_tables.down.fizz new file mode 100644 index 00000000..d0f82ee2 --- /dev/null +++ b/testdata/migrations/20210104145901_context_tables.down.fizz @@ -0,0 +1,2 @@ +drop_table("context_prefix_a_table") +drop_table("context_prefix_b_table") diff --git a/testdata/migrations/20210104145901_context_tables.up.fizz b/testdata/migrations/20210104145901_context_tables.up.fizz new file mode 100644 index 00000000..ae94796f --- /dev/null +++ b/testdata/migrations/20210104145901_context_tables.up.fizz @@ -0,0 +1,9 @@ +create_table("context_prefix_a_table") { + t.Column("id", "string", { primary: true }) + t.Column("value", "string") +} + +create_table("context_prefix_b_table") { + t.Column("id", "string", { primary: true }) + t.Column("value", "string") +} diff --git a/testdata/models/ac/user.go b/testdata/models/ac/user.go new file mode 100644 index 00000000..92335a16 --- /dev/null +++ b/testdata/models/ac/user.go @@ -0,0 +1,9 @@ +package ac + +import "context" + +type User struct{} + +func (u User) TableName(ctx context.Context) string { + return ctx.Value("name").(string) + "_useras" +} diff --git a/testdata/models/bc/user.go b/testdata/models/bc/user.go new file mode 100644 index 00000000..4b1c6257 --- /dev/null +++ b/testdata/models/bc/user.go @@ -0,0 +1,9 @@ +package bc + +import "context" + +type User struct{} + +func (u User) TableName(ctx context.Context) string { + return ctx.Value("name").(string) + "_userbs" +} diff --git a/validations.go b/validations.go index 79e76105..ab5b845e 100644 --- a/validations.go +++ b/validations.go @@ -133,7 +133,7 @@ func (m *Model) iterateAndValidate(fn modelIterableValidator) (*validate.Errors, if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { for i := 0; i < v.Len(); i++ { val := v.Index(i) - newModel := &Model{Value: val.Addr().Interface()} + newModel := NewModel(val.Addr().Interface(), m.ctx) verrs, err := fn(newModel) if err != nil || verrs.HasAny() {