Skip to content

Commit

Permalink
Latest from development (#617)
Browse files Browse the repository at this point in the history
* 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 <stanislas.michalak@gmail.com>
Co-authored-by: Larry M Jordan <larrymoralesjordan@gmail.com>

Co-authored-by: Patrik <zepatrik@users.noreply.github.com>
Co-authored-by: Michael Montgomery <mmontg1@gmail.com>
Co-authored-by: kyrozetera <jasonhale.w@gmail.com>
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 <stanislas.michalak@gmail.com>
Co-authored-by: Larry M Jordan <larrymoralesjordan@gmail.com>
  • Loading branch information
8 people committed Jan 27, 2021
1 parent 6a95bfb commit c81c996
Show file tree
Hide file tree
Showing 43 changed files with 664 additions and 185 deletions.
8 changes: 4 additions & 4 deletions belongs_to.go
Expand Up @@ -19,15 +19,15 @@ 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
}

// 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
}
Expand All @@ -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
}
7 changes: 4 additions & 3 deletions belongs_to_test.go
@@ -1,6 +1,7 @@
package pop

import (
"context"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
14 changes: 8 additions & 6 deletions columns/columns.go
Expand Up @@ -13,6 +13,7 @@ type Columns struct {
lock *sync.RWMutex
TableName string
TableAlias string
IDField string
}

// Add a column to the list.
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
}
10 changes: 5 additions & 5 deletions columns/columns_for_struct.go
Expand Up @@ -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("*")
}
}()
Expand Down
46 changes: 40 additions & 6 deletions columns/columns_test.go
Expand Up @@ -21,16 +21,16 @@ 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())
}

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"})
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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"])
}
6 changes: 3 additions & 3 deletions columns/readable_columns_test.go
Expand Up @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand Down
8 changes: 4 additions & 4 deletions columns/writeable_columns_test.go
Expand Up @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand Down
10 changes: 10 additions & 0 deletions connection.go
Expand Up @@ -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()
Expand Down
7 changes: 6 additions & 1 deletion connection_details.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
Expand Down
19 changes: 19 additions & 0 deletions connection_details_test.go
Expand Up @@ -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())
}
1 change: 1 addition & 0 deletions connection_instrumented.go
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion connection_instrumented_nosqlite_test.go
Expand Up @@ -3,8 +3,9 @@
package pop

import (
"github.com/stretchr/testify/require"
"testing"

"github.com/stretchr/testify/require"
)

func TestInstrumentation_WithoutSqlite(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions connection_instrumented_test.go
Expand Up @@ -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) {
Expand Down

0 comments on commit c81c996

Please sign in to comment.