From 5854df9b107db4cc29eb45d54707b4dc891eeb98 Mon Sep 17 00:00:00 2001 From: Mark Bates Date: Thu, 15 Oct 2015 13:52:55 -0400 Subject: [PATCH] Added support for BelongsToThrough --- belongs_to.go | 23 ++-- belongs_to_test.go | 20 +++ clause.go | 7 + columns.go | 213 ------------------------------ columns/column.go | 20 +++ columns/column_test.go | 14 ++ columns/columns.go | 107 +++++++++++++++ columns/columns_for_struct.go | 60 +++++++++ columns/columns_test.go | 62 +++++++++ columns/readable_columns.go | 19 +++ columns/readable_columns_test.go | 35 +++++ columns/writeable_columns.go | 19 +++ columns/writeable_columns_test.go | 35 +++++ columns_test.go | 121 ----------------- dialect.go | 2 + executors.go | 6 +- finders_test.go | 1 - model.go | 5 + mysql.go | 1 + pop_test.go | 1 + postgresql.go | 1 + query.go | 15 ++- query_test.go | 2 +- scopes_test.go | 2 +- sql_builder.go | 42 ++++-- sqlite.go | 1 + 26 files changed, 471 insertions(+), 363 deletions(-) delete mode 100644 columns.go create mode 100644 columns/column.go create mode 100644 columns/column_test.go create mode 100644 columns/columns.go create mode 100644 columns/columns_for_struct.go create mode 100644 columns/columns_test.go create mode 100644 columns/readable_columns.go create mode 100644 columns/readable_columns_test.go create mode 100644 columns/writeable_columns.go create mode 100644 columns/writeable_columns_test.go delete mode 100644 columns_test.go diff --git a/belongs_to.go b/belongs_to.go index 5655efd6..eb65417d 100644 --- a/belongs_to.go +++ b/belongs_to.go @@ -1,10 +1,6 @@ package pop -import ( - "fmt" - - "github.com/markbates/inflect" -) +import "fmt" func (c *Connection) BelongsTo(model interface{}) *Query { return Q(c).BelongsTo(model) @@ -12,9 +8,18 @@ func (c *Connection) BelongsTo(model interface{}) *Query { func (q *Query) BelongsTo(model interface{}) *Query { m := &Model{Value: model} - tn := m.TableName() - tn = inflect.Singularize(tn) - args := []interface{}{m.ID()} - q.WhereClauses = append(q.WhereClauses, Clause{fmt.Sprintf("%s_id = ?", tn), args}) + q.Where(fmt.Sprintf("%s = ?", m.AssociationName()), m.ID()) + return q +} + +func (c *Connection) BelongsToThrough(bt, thru interface{}) *Query { + return Q(c).BelongsToThrough(bt, thru) +} + +func (q *Query) BelongsToThrough(bt, thru interface{}) *Query { + q.BelongsToThroughClauses = append(q.BelongsToThroughClauses, BelongsToThroughClause{ + BelongsTo: &Model{Value: bt}, + Through: &Model{Value: thru}, + }) return q } diff --git a/belongs_to_test.go b/belongs_to_test.go index a81005ec..e124bd4c 100644 --- a/belongs_to_test.go +++ b/belongs_to_test.go @@ -1,6 +1,9 @@ package pop_test import ( + "fmt" + "os" + "strings" "testing" "github.com/markbates/pop" @@ -21,3 +24,20 @@ func Test_BelongsTo(t *testing.T) { r.Equal(cl.Arguments, []interface{}{1}) }) } + +func Test_BelongsToThrough(t *testing.T) { + transaction(func(tx *pop.Connection) { + r := require.New(t) + + q := tx.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" + if os.Getenv("SODA_DIALECT") == "postgres" { + qs = strings.Replace(qs, "?", "$1", -1) + } + + sql, args := q.ToSQL(&pop.Model{Value: &Enemy{}}) + r.Equal(qs, sql) + fmt.Printf("args: %s\n", args) + r.Equal([]interface{}{1}, args) + }) +} diff --git a/clause.go b/clause.go index 50c7e857..ee9e4796 100644 --- a/clause.go +++ b/clause.go @@ -47,3 +47,10 @@ func (c FromClauses) String() string { } return strings.Join(cs, ", ") } + +type BelongsToThroughClause struct { + BelongsTo *Model + Through *Model +} + +type BelongsToThroughClauses []BelongsToThroughClause diff --git a/columns.go b/columns.go deleted file mode 100644 index 354c118e..00000000 --- a/columns.go +++ /dev/null @@ -1,213 +0,0 @@ -package pop - -import ( - "fmt" - "reflect" - "sort" - "strings" - "sync" -) - -// Add a column to the list. -func (c *Columns) Add(names ...string) []*Column { - ret := []*Column{} - c.lock.Lock() - for _, name := range names { - xs := strings.Split(name, ",") - col := c.Cols[xs[0]] - if col == nil { - col = &Column{ - Name: xs[0], - SelectSQL: xs[0], - Readable: true, - Writeable: true, - } - - if len(xs) > 1 { - if xs[1] == "r" { - col.Writeable = false - } - if xs[1] == "w" { - col.Readable = false - } - } else if col.Name == "id" { - col.Writeable = false - } - - c.Cols[col.Name] = col - } - ret = append(ret, col) - } - c.lock.Unlock() - return ret -} - -// Remove a column from the list. -func (c *Columns) Remove(names ...string) { - for _, name := range names { - xs := strings.Split(name, ",") - name = xs[0] - delete(c.Cols, name) - } -} - -type Column struct { - Name string - Writeable bool - Readable bool - SelectSQL string -} - -func (c Column) UpdateString() string { - return fmt.Sprintf("%s = :%s", c.Name, c.Name) -} - -func (c *Column) SetSelectSQL(s string) { - c.SelectSQL = s - c.Writeable = false - c.Readable = true -} - -type WriteableColumns struct { - Columns -} - -func (c WriteableColumns) UpdateString() string { - xs := []string{} - for _, t := range c.Cols { - xs = append(xs, t.UpdateString()) - } - sort.Strings(xs) - return strings.Join(xs, ", ") -} - -type ReadableColumns struct { - Columns -} - -func (c ReadableColumns) SelectString() string { - xs := []string{} - for _, t := range c.Cols { - xs = append(xs, t.SelectSQL) - } - sort.Strings(xs) - return strings.Join(xs, ", ") -} - -type Columns struct { - Cols map[string]*Column - lock *sync.RWMutex -} - -func (c Columns) Writeable() *WriteableColumns { - w := &WriteableColumns{NewColumns()} - for _, col := range c.Cols { - if col.Writeable { - w.Cols[col.Name] = col - } - } - return w -} - -func (c Columns) Readable() *ReadableColumns { - w := &ReadableColumns{NewColumns()} - for _, col := range c.Cols { - if col.Readable { - w.Cols[col.Name] = col - } - } - return w -} - -func (c Columns) String() string { - xs := []string{} - for _, t := range c.Cols { - xs = append(xs, t.Name) - } - sort.Strings(xs) - return strings.Join(xs, ", ") -} - -func (c Columns) SymbolizedString() string { - xs := []string{} - for _, t := range c.Cols { - xs = append(xs, ":"+t.Name) - } - sort.Strings(xs) - return strings.Join(xs, ", ") -} - -func NewColumns() Columns { - return Columns{ - lock: &sync.RWMutex{}, - Cols: map[string]*Column{}, - } -} - -var columnsCache = map[string]Columns{} - -// ColumnsForStruct returns a Columns instance for -// the struct passed in. -func ColumnsForStruct(s interface{}) (columns Columns) { - columns = NewColumns() - defer func() { - if r := recover(); r != nil { - columns = NewColumns() - columns.Add("*") - } - }() - st := reflect.TypeOf(s) - if st.Kind() == reflect.Ptr { - st = reflect.ValueOf(s).Elem().Type() - } - if st.Kind() == reflect.Slice { - v := reflect.ValueOf(s) - t := v.Type() - x := t.Elem().Elem() - - n := reflect.New(x) - return ColumnsForStruct(n.Interface()) - } - - key := strings.Join([]string{st.PkgPath(), st.Name()}, ".") - // if cc, ok := columnsCache[key]; ok { - // ccols := map[string]*Column{} - // for k, v := range cc.Cols { - // ccols[k] = v - // } - // return Columns{ - // lock: &sync.RWMutex{}, - // Cols: ccols, - // } - // // return cc - // } - // fmt.Printf("st.PkgPath(): %s\n", st.PkgPath()) - // fmt.Printf("st.Name(): %s\n", st.Name()) - - field_count := st.NumField() - - for i := 0; i < field_count; i++ { - field := st.Field(i) - tag := field.Tag.Get("db") - if tag == "" { - tag = field.Name - } - - if tag != "-" { - rw := field.Tag.Get("rw") - if rw != "" { - tag = tag + "," + rw - } - cs := columns.Add(tag) - c := cs[0] - tag = field.Tag.Get("select") - if tag != "" { - c.SetSelectSQL(tag) - } - } - } - - columnsCache[key] = columns - - return columns -} diff --git a/columns/column.go b/columns/column.go new file mode 100644 index 00000000..63eb5370 --- /dev/null +++ b/columns/column.go @@ -0,0 +1,20 @@ +package columns + +import "fmt" + +type Column struct { + Name string + Writeable bool + Readable bool + SelectSQL string +} + +func (c Column) UpdateString() string { + return fmt.Sprintf("%s = :%s", c.Name, c.Name) +} + +func (c *Column) SetSelectSQL(s string) { + c.SelectSQL = s + c.Writeable = false + c.Readable = true +} diff --git a/columns/column_test.go b/columns/column_test.go new file mode 100644 index 00000000..892bd23b --- /dev/null +++ b/columns/column_test.go @@ -0,0 +1,14 @@ +package columns_test + +import ( + "testing" + + "github.com/markbates/pop/columns" + "github.com/stretchr/testify/require" +) + +func Test_Column_UpdateString(t *testing.T) { + r := require.New(t) + c := columns.Column{Name: "foo"} + r.Equal(c.UpdateString(), "foo = :foo") +} diff --git a/columns/columns.go b/columns/columns.go new file mode 100644 index 00000000..f1bf6e7b --- /dev/null +++ b/columns/columns.go @@ -0,0 +1,107 @@ +package columns + +import ( + "fmt" + "sort" + "strings" + "sync" +) + +type Columns struct { + Cols map[string]*Column + lock *sync.RWMutex + TableName string +} + +// Add a column to the list. +func (c *Columns) Add(names ...string) []*Column { + ret := []*Column{} + c.lock.Lock() + for _, name := range names { + xs := strings.Split(name, ",") + col := c.Cols[xs[0]] + if col == nil { + ss := xs[0] + if c.TableName != "" { + ss = fmt.Sprintf("%s.%s", c.TableName, ss) + } + col = &Column{ + Name: xs[0], + SelectSQL: ss, + Readable: true, + Writeable: true, + } + + if len(xs) > 1 { + if xs[1] == "r" { + col.Writeable = false + } + if xs[1] == "w" { + col.Readable = false + } + } else if col.Name == "id" { + col.Writeable = false + } + + c.Cols[col.Name] = col + } + ret = append(ret, col) + } + c.lock.Unlock() + return ret +} + +// Remove a column from the list. +func (c *Columns) Remove(names ...string) { + for _, name := range names { + xs := strings.Split(name, ",") + name = xs[0] + delete(c.Cols, name) + } +} + +func (c Columns) Writeable() *WriteableColumns { + w := &WriteableColumns{NewColumns(c.TableName)} + for _, col := range c.Cols { + if col.Writeable { + w.Cols[col.Name] = col + } + } + return w +} + +func (c Columns) Readable() *ReadableColumns { + w := &ReadableColumns{NewColumns(c.TableName)} + for _, col := range c.Cols { + if col.Readable { + w.Cols[col.Name] = col + } + } + return w +} + +func (c Columns) String() string { + xs := []string{} + for _, t := range c.Cols { + xs = append(xs, t.Name) + } + sort.Strings(xs) + return strings.Join(xs, ", ") +} + +func (c Columns) SymbolizedString() string { + xs := []string{} + for _, t := range c.Cols { + xs = append(xs, ":"+t.Name) + } + sort.Strings(xs) + return strings.Join(xs, ", ") +} + +func NewColumns(tableName string) Columns { + return Columns{ + lock: &sync.RWMutex{}, + Cols: map[string]*Column{}, + TableName: tableName, + } +} diff --git a/columns/columns_for_struct.go b/columns/columns_for_struct.go new file mode 100644 index 00000000..1c4bce61 --- /dev/null +++ b/columns/columns_for_struct.go @@ -0,0 +1,60 @@ +package columns + +import ( + "reflect" + "strings" +) + +var columnsCache = map[string]Columns{} + +// ColumnsForStruct returns a Columns instance for +// the struct passed in. +func ColumnsForStruct(s interface{}, tableName string) (columns Columns) { + columns = NewColumns(tableName) + defer func() { + if r := recover(); r != nil { + columns = NewColumns(tableName) + columns.Add("*") + } + }() + st := reflect.TypeOf(s) + if st.Kind() == reflect.Ptr { + st = reflect.ValueOf(s).Elem().Type() + } + if st.Kind() == reflect.Slice { + v := reflect.ValueOf(s) + t := v.Type() + x := t.Elem().Elem() + + n := reflect.New(x) + return ColumnsForStruct(n.Interface(), tableName) + } + + key := strings.Join([]string{st.PkgPath(), st.Name()}, ".") + field_count := st.NumField() + + for i := 0; i < field_count; i++ { + field := st.Field(i) + tag := field.Tag.Get("db") + if tag == "" { + tag = field.Name + } + + if tag != "-" { + rw := field.Tag.Get("rw") + if rw != "" { + tag = tag + "," + rw + } + cs := columns.Add(tag) + c := cs[0] + tag = field.Tag.Get("select") + if tag != "" { + c.SetSelectSQL(tag) + } + } + } + + columnsCache[key] = columns + + return columns +} diff --git a/columns/columns_test.go b/columns/columns_test.go new file mode 100644 index 00000000..a26d8590 --- /dev/null +++ b/columns/columns_test.go @@ -0,0 +1,62 @@ +package columns_test + +import ( + "testing" + + "github.com/markbates/pop/columns" + "github.com/stretchr/testify/require" +) + +type foo struct { + FirstName string `db:"first_name" select:"first_name as f"` + LastName string + Unwanted string `db:"-"` + ReadOnly string `db:"read" rw:"r"` + WriteOnly string `db:"write" rw:"w"` +} + +type foos []foo + +func Test_Column_MapsSlice(t *testing.T) { + r := require.New(t) + + c1 := columns.ColumnsForStruct(&foo{}, "foo") + c2 := columns.ColumnsForStruct(&foos{}, "foo") + 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.ColumnsForStruct(f, "foo") + 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"}) + r.Equal(c.Cols["read"], &columns.Column{Name: "read", Writeable: false, Readable: true, SelectSQL: "foo.read"}) + r.Equal(c.Cols["write"], &columns.Column{Name: "write", Writeable: true, Readable: false, SelectSQL: "foo.write"}) + } +} + +func Test_Columns_Add(t *testing.T) { + r := require.New(t) + + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + r.Equal(len(c.Cols), 4) + c.Add("foo", "first_name") + r.Equal(len(c.Cols), 5) + r.Equal(c.Cols["foo"], &columns.Column{Name: "foo", Writeable: true, Readable: true, SelectSQL: "foo.foo"}) + } +} + +func Test_Columns_Remove(t *testing.T) { + r := require.New(t) + + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + r.Equal(len(c.Cols), 4) + c.Remove("foo", "first_name") + r.Equal(len(c.Cols), 3) + } +} diff --git a/columns/readable_columns.go b/columns/readable_columns.go new file mode 100644 index 00000000..f71fae74 --- /dev/null +++ b/columns/readable_columns.go @@ -0,0 +1,19 @@ +package columns + +import ( + "sort" + "strings" +) + +type ReadableColumns struct { + Columns +} + +func (c ReadableColumns) SelectString() string { + xs := []string{} + for _, t := range c.Cols { + xs = append(xs, t.SelectSQL) + } + sort.Strings(xs) + return strings.Join(xs, ", ") +} diff --git a/columns/readable_columns_test.go b/columns/readable_columns_test.go new file mode 100644 index 00000000..38347104 --- /dev/null +++ b/columns/readable_columns_test.go @@ -0,0 +1,35 @@ +package columns_test + +import ( + "testing" + + "github.com/markbates/pop/columns" + "github.com/stretchr/testify/require" +) + +func Test_Columns_ReadableString(t *testing.T) { + r := require.New(t) + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + u := c.Readable().String() + r.Equal(u, "LastName, first_name, read") + } +} + +func Test_Columns_Readable_SelectString(t *testing.T) { + r := require.New(t) + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + u := c.Readable().SelectString() + r.Equal(u, "first_name as f, foo.LastName, foo.read") + } +} + +func Test_Columns_ReadableString_Symbolized(t *testing.T) { + r := require.New(t) + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + u := c.Readable().SymbolizedString() + r.Equal(u, ":LastName, :first_name, :read") + } +} diff --git a/columns/writeable_columns.go b/columns/writeable_columns.go new file mode 100644 index 00000000..593df655 --- /dev/null +++ b/columns/writeable_columns.go @@ -0,0 +1,19 @@ +package columns + +import ( + "sort" + "strings" +) + +type WriteableColumns struct { + Columns +} + +func (c WriteableColumns) UpdateString() string { + xs := []string{} + for _, t := range c.Cols { + xs = append(xs, t.UpdateString()) + } + sort.Strings(xs) + return strings.Join(xs, ", ") +} diff --git a/columns/writeable_columns_test.go b/columns/writeable_columns_test.go new file mode 100644 index 00000000..ed6a47b3 --- /dev/null +++ b/columns/writeable_columns_test.go @@ -0,0 +1,35 @@ +package columns_test + +import ( + "testing" + + "github.com/markbates/pop/columns" + "github.com/stretchr/testify/require" +) + +func Test_Columns_WriteableString_Symbolized(t *testing.T) { + r := require.New(t) + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + u := c.Writeable().SymbolizedString() + r.Equal(u, ":LastName, :write") + } +} + +func Test_Columns_UpdateString(t *testing.T) { + r := require.New(t) + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + u := c.Writeable().UpdateString() + r.Equal(u, "LastName = :LastName, write = :write") + } +} + +func Test_Columns_WriteableString(t *testing.T) { + r := require.New(t) + for _, f := range []interface{}{foo{}, &foo{}} { + c := columns.ColumnsForStruct(f, "foo") + u := c.Writeable().String() + r.Equal(u, "LastName, write") + } +} diff --git a/columns_test.go b/columns_test.go deleted file mode 100644 index 10c325a3..00000000 --- a/columns_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package pop_test - -import ( - "testing" - - "github.com/markbates/pop" - "github.com/stretchr/testify/require" -) - -type foo struct { - FirstName string `db:"first_name" select:"first_name as f"` - LastName string - Unwanted string `db:"-"` - ReadOnly string `db:"read" rw:"r"` - WriteOnly string `db:"write" rw:"w"` -} - -type foos []foo - -func Test_Column_MapsSlice(t *testing.T) { - r := require.New(t) - - c1 := pop.ColumnsForStruct(&foo{}) - c2 := pop.ColumnsForStruct(&foos{}) - r.Equal(c1.String(), c2.String()) -} - -func Test_Column_UpdateString(t *testing.T) { - r := require.New(t) - c := pop.Column{Name: "foo"} - r.Equal(c.UpdateString(), "foo = :foo") -} - -func Test_Columns_UpdateString(t *testing.T) { - r := require.New(t) - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - u := c.Writeable().UpdateString() - r.Equal(u, "LastName = :LastName, write = :write") - } -} - -func Test_Columns_WriteableString(t *testing.T) { - r := require.New(t) - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - u := c.Writeable().String() - r.Equal(u, "LastName, write") - } -} - -func Test_Columns_ReadableString(t *testing.T) { - r := require.New(t) - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - u := c.Readable().String() - r.Equal(u, "LastName, first_name, read") - } -} - -func Test_Columns_Readable_SelectString(t *testing.T) { - r := require.New(t) - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - u := c.Readable().SelectString() - r.Equal(u, "LastName, first_name as f, read") - } -} - -func Test_Columns_WriteableString_Symbolized(t *testing.T) { - r := require.New(t) - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - u := c.Writeable().SymbolizedString() - r.Equal(u, ":LastName, :write") - } -} - -func Test_Columns_ReadableString_Symbolized(t *testing.T) { - r := require.New(t) - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - u := c.Readable().SymbolizedString() - r.Equal(u, ":LastName, :first_name, :read") - } -} -func Test_Columns_Basics(t *testing.T) { - r := require.New(t) - - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - r.Equal(len(c.Cols), 4) - r.Equal(c.Cols["first_name"], &pop.Column{Name: "first_name", Writeable: false, Readable: true, SelectSQL: "first_name as f"}) - r.Equal(c.Cols["LastName"], &pop.Column{Name: "LastName", Writeable: true, Readable: true, SelectSQL: "LastName"}) - r.Equal(c.Cols["read"], &pop.Column{Name: "read", Writeable: false, Readable: true, SelectSQL: "read"}) - r.Equal(c.Cols["write"], &pop.Column{Name: "write", Writeable: true, Readable: false, SelectSQL: "write"}) - } -} - -func Test_Columns_Add(t *testing.T) { - r := require.New(t) - - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - r.Equal(len(c.Cols), 4) - c.Add("foo", "first_name") - r.Equal(len(c.Cols), 5) - r.Equal(c.Cols["foo"], &pop.Column{Name: "foo", Writeable: true, Readable: true, SelectSQL: "foo"}) - } -} - -func Test_Columns_Remove(t *testing.T) { - r := require.New(t) - - for _, f := range []interface{}{foo{}, &foo{}} { - c := pop.ColumnsForStruct(f) - r.Equal(len(c.Cols), 4) - c.Remove("foo", "first_name") - r.Equal(len(c.Cols), 3) - } -} diff --git a/dialect.go b/dialect.go index a45cf0f6..419957be 100644 --- a/dialect.go +++ b/dialect.go @@ -3,6 +3,8 @@ package pop import ( "fmt" "log" + + . "github.com/markbates/pop/columns" ) type Dialect interface { diff --git a/executors.go b/executors.go index 0b6c083c..3538c7e0 100644 --- a/executors.go +++ b/executors.go @@ -1,5 +1,7 @@ package pop +import . "github.com/markbates/pop/columns" + func (c *Connection) Reload(model interface{}) error { sm := Model{Value: model} return c.Find(model, sm.ID()) @@ -26,7 +28,7 @@ func (c *Connection) Create(model interface{}, excludeColumns ...string) error { return c.timeFunc("Create", func() error { sm := &Model{Value: model} - cols := ColumnsForStruct(model) + cols := ColumnsForStruct(model, sm.TableName()) cols.Remove(excludeColumns...) sm.TouchCreatedAt() @@ -40,7 +42,7 @@ func (c *Connection) Update(model interface{}, excludeColumns ...string) error { return c.timeFunc("Update", func() error { sm := &Model{Value: model} - cols := ColumnsForStruct(model) + cols := ColumnsForStruct(model, sm.TableName()) cols.Remove("id", "created_at") cols.Remove(excludeColumns...) diff --git a/finders_test.go b/finders_test.go index 4eccb158..4ea34a26 100644 --- a/finders_test.go +++ b/finders_test.go @@ -94,7 +94,6 @@ func Test_Count(t *testing.T) { user := User{Name: nulls.NewString("Mark")} err := tx.Create(&user) a.NoError(err) - c, err := tx.Count(&user) a.NoError(err) a.Equal(c, 1) diff --git a/model.go b/model.go index bc6b8bad..8d7ee71f 100644 --- a/model.go +++ b/model.go @@ -31,6 +31,11 @@ func (m *Model) FieldByName(s string) (reflect.Value, error) { return fbn, nil } +func (m *Model) AssociationName() string { + tn := inflect.Singularize(m.TableName()) + return fmt.Sprintf("%s_id", tn) +} + func (m *Model) Validate(*Connection) (*validate.Errors, error) { return validate.NewErrors(), nil } diff --git a/mysql.go b/mysql.go index 1bab2d23..1b662b49 100644 --- a/mysql.go +++ b/mysql.go @@ -6,6 +6,7 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/markbates/going/clam" + . "github.com/markbates/pop/columns" ) type MySQL struct { diff --git a/pop_test.go b/pop_test.go index a924f27e..fe500e5f 100644 --- a/pop_test.go +++ b/pop_test.go @@ -58,4 +58,5 @@ type Friend struct { type Friends []Friend type Enemy struct { + A string } diff --git a/postgresql.go b/postgresql.go index e46bb6d3..8f5bf040 100644 --- a/postgresql.go +++ b/postgresql.go @@ -8,6 +8,7 @@ import ( _ "github.com/lib/pq" "github.com/markbates/going/clam" + . "github.com/markbates/pop/columns" ) type PostgreSQL struct { diff --git a/query.go b/query.go index ad841fcb..932a8272 100644 --- a/query.go +++ b/query.go @@ -1,13 +1,14 @@ package pop type Query struct { - RawSQL *Clause - LimitResults int - WhereClauses Clauses - OrderClauses Clauses - FromClauses FromClauses - Paginator *Paginator - Connection *Connection + RawSQL *Clause + LimitResults int + WhereClauses Clauses + OrderClauses Clauses + FromClauses FromClauses + BelongsToThroughClauses BelongsToThroughClauses + Paginator *Paginator + Connection *Connection } func (c *Connection) RawQuery(stmt string, args ...interface{}) *Query { diff --git a/query_test.go b/query_test.go index 8841971f..5ad9a32f 100644 --- a/query_test.go +++ b/query_test.go @@ -51,7 +51,7 @@ func Test_ToSQL(t *testing.T) { transaction(func(tx *pop.Connection) { user := &pop.Model{Value: &User{}} - s := "SELECT alive, bio, birth_date, created_at, id, name, name as full_name, price, updated_at FROM users AS users" + s := "SELECT name as full_name, users.alive, users.bio, users.birth_date, users.created_at, users.id, users.name, users.price, users.updated_at FROM users AS users" query := pop.Q(tx) q, _ := query.ToSQL(user) diff --git a/scopes_test.go b/scopes_test.go index 2aa6fac6..6b2505ee 100644 --- a/scopes_test.go +++ b/scopes_test.go @@ -10,7 +10,7 @@ import ( func Test_Scopes(t *testing.T) { r := require.New(t) - oql := "SELECT alive, bio, birth_date, created_at, id, name, name as full_name, price, updated_at FROM users AS users" + oql := "SELECT name as full_name, users.alive, users.bio, users.birth_date, users.created_at, users.id, users.name, users.price, users.updated_at FROM users AS users" transaction(func(tx *pop.Connection) { u := &pop.Model{Value: &User{}} diff --git a/sql_builder.go b/sql_builder.go index fc9b9ee6..6377d2f6 100644 --- a/sql_builder.go +++ b/sql_builder.go @@ -4,6 +4,8 @@ import ( "fmt" "log" "strings" + + . "github.com/markbates/pop/columns" ) type SQLBuilder struct { @@ -56,7 +58,7 @@ func (sq *SQLBuilder) compile() { func (sq *SQLBuilder) log() { if Debug { - args := sq.Args() + args := sq.args x := fmt.Sprintf("[POP]: %s", sq.sql) if len(args) > 0 { @@ -76,22 +78,46 @@ func (sq *SQLBuilder) log() { } func (sq *SQLBuilder) buildSQL() string { - tableName := sq.Model.TableName() cols := sq.buildColumns() - fc := append(sq.Query.FromClauses, FromClause{ - From: tableName, - As: strings.Replace(tableName, ".", "_", -1), - }) + fc := sq.buildFromClauses() sql := fmt.Sprintf("SELECT %s FROM %s", cols.Readable().SelectString(), fc) + sql = sq.buildWhereClauses(sql) sql = sq.buildOrderClauses(sql) sql = sq.buildPaginationClauses(sql) + return sql } +func (sq *SQLBuilder) buildFromClauses() FromClauses { + models := []*Model{ + sq.Model, + } + for _, mc := range sq.Query.BelongsToThroughClauses { + models = append(models, mc.Through) + } + + fc := sq.Query.FromClauses + for _, m := range models { + tableName := m.TableName() + fc = append(fc, FromClause{ + From: tableName, + As: strings.Replace(tableName, ".", "_", -1), + }) + } + + return fc +} + func (sq *SQLBuilder) buildWhereClauses(sql string) string { + mcs := sq.Query.BelongsToThroughClauses + for _, mc := range mcs { + sq.Query.Where(fmt.Sprintf("%s.%s = ?", mc.Through.TableName(), mc.BelongsTo.AssociationName()), mc.BelongsTo.ID()) + sq.Query.Where(fmt.Sprintf("%s.id = %s.%s", sq.Model.TableName(), mc.Through.TableName(), sq.Model.AssociationName())) + } + wc := sq.Query.WhereClauses if len(wc) > 0 { sql = fmt.Sprintf("%s WHERE %s", sql, wc.Join(" AND ")) @@ -134,11 +160,11 @@ func (sq *SQLBuilder) buildColumns() Columns { if ok { return cols } - cols = ColumnsForStruct(sq.Model.Value) + cols = ColumnsForStruct(sq.Model.Value, tableName) columnCache[tableName] = cols return cols } else { - cols := NewColumns() + cols := NewColumns("") cols.Add(sq.AddColumns...) return cols } diff --git a/sqlite.go b/sqlite.go index d2971dbd..309f57fd 100644 --- a/sqlite.go +++ b/sqlite.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" + . "github.com/markbates/pop/columns" _ "github.com/mattn/go-sqlite3" )