Skip to content

Commit cddc40e

Browse files
authored
Merge pull request #171 from doug-martin/v9.4.0-rc
v9.4.0
2 parents b80d936 + 6d0ed44 commit cddc40e

13 files changed

Lines changed: 1092 additions & 52 deletions

HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# v9.4.0
2+
3+
* [ADDED] Ability to scan into struct fields from multiple tables [#160](https://github.com/doug-martin/goqu/issues/160)
4+
15
# v9.3.0
26

37
* [ADDED] Using Update, Insert, or Delete datasets in sub selects and CTEs [#164](https://github.com/doug-martin/goqu/issues/164)

database_example_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,27 +109,27 @@ func ExampleDatabase_Dialect() {
109109
func ExampleDatabase_Exec() {
110110
db := getDb()
111111

112-
_, err := db.Exec(`DROP TABLE "goqu_user"`)
112+
_, err := db.Exec(`DROP TABLE "user_role"; DROP TABLE "goqu_user"`)
113113
if err != nil {
114-
fmt.Println("Error occurred while dropping table", err.Error())
114+
fmt.Println("Error occurred while dropping tables", err.Error())
115115
}
116-
fmt.Println("Dropped table goqu_user")
116+
fmt.Println("Dropped tables user_role and goqu_user")
117117
// Output:
118-
// Dropped table goqu_user
118+
// Dropped tables user_role and goqu_user
119119
}
120120

121121
func ExampleDatabase_ExecContext() {
122122
db := getDb()
123123
d := time.Now().Add(50 * time.Millisecond)
124124
ctx, cancel := context.WithDeadline(context.Background(), d)
125125
defer cancel()
126-
_, err := db.ExecContext(ctx, `DROP TABLE "goqu_user"`)
126+
_, err := db.ExecContext(ctx, `DROP TABLE "user_role"; DROP TABLE "goqu_user"`)
127127
if err != nil {
128-
fmt.Println("Error occurred while dropping table", err.Error())
128+
fmt.Println("Error occurred while dropping tables", err.Error())
129129
}
130-
fmt.Println("Dropped table goqu_user")
130+
fmt.Println("Dropped tables user_role and goqu_user")
131131
// Output:
132-
// Dropped table goqu_user
132+
// Dropped tables user_role and goqu_user
133133
}
134134

135135
func ExampleDatabase_From() {

docs/selecting.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,78 @@ if err := db.From("user").Select("first_name").ScanStructs(&users); err != nil{
842842
fmt.Printf("\n%+v", users)
843843
```
844844

845+
`goqu` also supports scanning into multiple structs. In the example below we define a `Role` and `User` struct that could both be used individually to scan into. However, you can also create a new struct that adds both structs as fields that can be populated in a single query.
846+
847+
**NOTE** When calling `ScanStructs` without a select already defined it will automatically only `SELECT` the columns found in the struct
848+
849+
```go
850+
type Role struct {
851+
Id uint64 `db:"id"`
852+
UserID uint64 `db:"user_id"`
853+
Name string `db:"name"`
854+
}
855+
type User struct {
856+
Id uint64 `db:"id"`
857+
FirstName string `db:"first_name"`
858+
LastName string `db:"last_name"`
859+
}
860+
type UserAndRole struct {
861+
User User `db:"goqu_user"` // tag as the "goqu_user" table
862+
Role Role `db:"user_role"` // tag as "user_role" table
863+
}
864+
db := getDb()
865+
866+
ds := db.
867+
From("goqu_user").
868+
Join(goqu.T("user_role"), goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id"))))
869+
var users []UserAndRole
870+
// Scan structs will auto build the
871+
if err := ds.ScanStructs(&users); err != nil {
872+
fmt.Println(err.Error())
873+
return
874+
}
875+
for _, u := range users {
876+
fmt.Printf("\n%+v", u)
877+
}
878+
```
879+
880+
You can alternatively manually select the columns with the appropriate aliases using the `goqu.C` method to create the alias.
881+
882+
```go
883+
type Role struct {
884+
UserID uint64 `db:"user_id"`
885+
Name string `db:"name"`
886+
}
887+
type User struct {
888+
Id uint64 `db:"id"`
889+
FirstName string `db:"first_name"`
890+
LastName string `db:"last_name"`
891+
Role Role `db:"user_role"` // tag as "user_role" table
892+
}
893+
db := getDb()
894+
895+
ds := db.
896+
Select(
897+
"goqu_user.id",
898+
"goqu_user.first_name",
899+
"goqu_user.last_name",
900+
// alias the fully qualified identifier `C` is important here so it doesnt parse it
901+
goqu.I("user_role.user_id").As(goqu.C("user_role.user_id")),
902+
goqu.I("user_role.name").As(goqu.C("user_role.name")),
903+
).
904+
From("goqu_user").
905+
Join(goqu.T("user_role"), goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id"))))
906+
907+
var users []User
908+
if err := ds.ScanStructs(&users); err != nil {
909+
fmt.Println(err.Error())
910+
return
911+
}
912+
for _, u := range users {
913+
fmt.Printf("\n%+v", u)
914+
}
915+
```
916+
845917
<a name="scan-struct"></a>
846918
**[`ScanStruct`](http://godoc.org/github.com/doug-martin/goqu#SelectDataset.ScanStruct)**
847919

@@ -869,6 +941,83 @@ if !found {
869941
}
870942
```
871943

944+
`goqu` also supports scanning into multiple structs. In the example below we define a `Role` and `User` struct that could both be used individually to scan into. However, you can also create a new struct that adds both structs as fields that can be populated in a single query.
945+
946+
**NOTE** When calling `ScanStruct` without a select already defined it will automatically only `SELECT` the columns found in the struct
947+
948+
```go
949+
type Role struct {
950+
UserID uint64 `db:"user_id"`
951+
Name string `db:"name"`
952+
}
953+
type User struct {
954+
ID uint64 `db:"id"`
955+
FirstName string `db:"first_name"`
956+
LastName string `db:"last_name"`
957+
}
958+
type UserAndRole struct {
959+
User User `db:"goqu_user"` // tag as the "goqu_user" table
960+
Role Role `db:"user_role"` // tag as "user_role" table
961+
}
962+
db := getDb()
963+
var userAndRole UserAndRole
964+
ds := db.
965+
From("goqu_user").
966+
Join(goqu.T("user_role"),goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id")))).
967+
Where(goqu.C("first_name").Eq("Bob"))
968+
969+
found, err := ds.ScanStruct(&userAndRole)
970+
if err != nil{
971+
fmt.Println(err.Error())
972+
return
973+
}
974+
if !found {
975+
fmt.Println("No user found")
976+
} else {
977+
fmt.Printf("\nFound user: %+v", user)
978+
}
979+
```
980+
981+
You can alternatively manually select the columns with the appropriate aliases using the `goqu.C` method to create the alias.
982+
983+
```go
984+
type Role struct {
985+
UserID uint64 `db:"user_id"`
986+
Name string `db:"name"`
987+
}
988+
type User struct {
989+
ID uint64 `db:"id"`
990+
FirstName string `db:"first_name"`
991+
LastName string `db:"last_name"`
992+
Role Role `db:"user_role"` // tag as "user_role" table
993+
}
994+
db := getDb()
995+
var userAndRole UserAndRole
996+
ds := db.
997+
Select(
998+
"goqu_user.id",
999+
"goqu_user.first_name",
1000+
"goqu_user.last_name",
1001+
// alias the fully qualified identifier `C` is important here so it doesnt parse it
1002+
goqu.I("user_role.user_id").As(goqu.C("user_role.user_id")),
1003+
goqu.I("user_role.name").As(goqu.C("user_role.name")),
1004+
).
1005+
From("goqu_user").
1006+
Join(goqu.T("user_role"),goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id")))).
1007+
Where(goqu.C("first_name").Eq("Bob"))
1008+
1009+
found, err := ds.ScanStruct(&userAndRole)
1010+
if err != nil{
1011+
fmt.Println(err.Error())
1012+
return
1013+
}
1014+
if !found {
1015+
fmt.Println("No user found")
1016+
} else {
1017+
fmt.Printf("\nFound user: %+v", user)
1018+
}
1019+
```
1020+
8721021

8731022
**NOTE** Using the `goqu.SetColumnRenameFunction` function, you can change the function that's used to rename struct fields when struct tags aren't defined
8741023

@@ -1037,3 +1186,4 @@ fmt.Printf("\nIds := %+v", ids)
10371186

10381187

10391188

1189+

exec/query_executor_test.go

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"encoding/json"
77
"fmt"
8+
"strings"
89
"testing"
910
"time"
1011

@@ -415,8 +416,8 @@ func (qes *queryExecutorSuite) TestScanStructs_withIgnoredEmbeddedPointerStruct(
415416
var composed []ComposedIgnoredPointerStruct
416417
qes.NoError(e.ScanStructs(&composed))
417418
qes.Equal([]ComposedIgnoredPointerStruct{
418-
{StructWithTags: &StructWithTags{}, PhoneNumber: testPhone1, Age: testAge1},
419-
{StructWithTags: &StructWithTags{}, PhoneNumber: testPhone2, Age: testAge2},
419+
{PhoneNumber: testPhone1, Age: testAge1},
420+
{PhoneNumber: testPhone2, Age: testAge2},
420421
}, composed)
421422
}
422423

@@ -944,6 +945,79 @@ func (qes *queryExecutorSuite) TestScanStruct() {
944945
}, noTag)
945946
}
946947

948+
func (qes *queryExecutorSuite) TestScanStruct_taggedStructs() {
949+
type StructWithNoTags struct {
950+
Address string
951+
Name string
952+
}
953+
954+
type StructWithTags struct {
955+
Address string `db:"address"`
956+
Name string `db:"name"`
957+
}
958+
959+
type ComposedStruct struct {
960+
StructWithTags
961+
PhoneNumber string `db:"phone_number"`
962+
Age int64 `db:"age"`
963+
}
964+
type ComposedWithPointerStruct struct {
965+
*StructWithTags
966+
PhoneNumber string `db:"phone_number"`
967+
Age int64 `db:"age"`
968+
}
969+
970+
type StructWithTaggedStructs struct {
971+
NoTags StructWithNoTags `db:"notags"`
972+
Tags StructWithTags `db:"tags"`
973+
Composed ComposedStruct `db:"composedstruct"`
974+
ComposedPointer ComposedWithPointerStruct `db:"composedptrstruct"`
975+
}
976+
977+
db, mock, err := sqlmock.New()
978+
qes.NoError(err)
979+
980+
cols := []string{
981+
"notags.address", "notags.name",
982+
"tags.address", "tags.name",
983+
"composedstruct.address", "composedstruct.name", "composedstruct.phone_number", "composedstruct.age",
984+
"composedptrstruct.address", "composedptrstruct.name", "composedptrstruct.phone_number", "composedptrstruct.age",
985+
}
986+
987+
q := `SELECT` + strings.Join(cols, ", ") + ` FROM "items"`
988+
989+
mock.ExpectQuery(q).
990+
WithArgs().
991+
WillReturnRows(sqlmock.NewRows(cols).AddRow(
992+
testAddr1, testName1,
993+
testAddr2, testName2,
994+
testAddr1, testName1, testPhone1, testAge1,
995+
testAddr2, testName2, testPhone2, testAge2,
996+
))
997+
998+
e := newQueryExecutor(db, nil, q)
999+
1000+
var item StructWithTaggedStructs
1001+
found, err := e.ScanStruct(&item)
1002+
qes.NoError(err)
1003+
qes.True(found)
1004+
qes.Equal(StructWithTaggedStructs{
1005+
NoTags: StructWithNoTags{Address: testAddr1, Name: testName1},
1006+
Tags: StructWithTags{Address: testAddr2, Name: testName2},
1007+
Composed: ComposedStruct{
1008+
StructWithTags: StructWithTags{Address: testAddr1, Name: testName1},
1009+
PhoneNumber: testPhone1,
1010+
Age: testAge1,
1011+
},
1012+
ComposedPointer: ComposedWithPointerStruct{
1013+
StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2},
1014+
PhoneNumber: testPhone2,
1015+
Age: testAge2,
1016+
},
1017+
}, item)
1018+
1019+
}
1020+
9471021
func (qes *queryExecutorSuite) TestScanVals() {
9481022
db, mock, err := sqlmock.New()
9491023
qes.NoError(err)

exp/col.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ func NewColumnListExpression(vals ...interface{}) ColumnListExpression {
3232
}
3333
structCols := cm.Cols()
3434
for _, col := range structCols {
35-
cols = append(cols, ParseIdentifier(col))
35+
i := ParseIdentifier(col)
36+
var sc Expression = i
37+
if i.IsQualified() {
38+
sc = i.As(NewIdentifierExpression("", "", col))
39+
}
40+
cols = append(cols, sc)
3641
}
3742
} else {
3843
panic(fmt.Sprintf("Cannot created expression from %+v", val))

exp/exp.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,14 @@ type (
252252
Updateable
253253
Distinctable
254254
Castable
255+
// returns true if this identifier has more more than on part (Schema, Table or Col)
256+
// "schema" -> true //cant qualify anymore
257+
// "schema.table" -> true
258+
// "table" -> false
259+
// "schema"."table"."col" -> true
260+
// "table"."col" -> true
261+
// "col" -> false
262+
IsQualified() bool
255263
// Returns a new IdentifierExpression with the specified schema
256264
Schema(string) IdentifierExpression
257265
// Returns the current schema

exp/ident.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ func (i identifier) Clone() Expression {
3333
return i.clone()
3434
}
3535

36+
func (i identifier) IsQualified() bool {
37+
schema, table, col := i.schema, i.table, i.col
38+
switch c := col.(type) {
39+
case string:
40+
if c != "" {
41+
return len(table) > 0 || len(schema) > 0
42+
}
43+
default:
44+
if c != nil {
45+
return len(table) > 0 || len(schema) > 0
46+
}
47+
}
48+
if len(table) > 0 {
49+
return len(schema) > 0
50+
}
51+
return false
52+
}
53+
3654
// Sets the table on the current identifier
3755
// I("col").Table("table") -> "table"."col" //postgres
3856
// I("col").Table("table") -> `table`.`col` //mysql

0 commit comments

Comments
 (0)