Skip to content

Commit

Permalink
Merge pull request #171 from doug-martin/v9.4.0-rc
Browse files Browse the repository at this point in the history
v9.4.0
  • Loading branch information
doug-martin authored Oct 1, 2019
2 parents b80d936 + 6d0ed44 commit cddc40e
Show file tree
Hide file tree
Showing 13 changed files with 1,092 additions and 52 deletions.
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v9.4.0

* [ADDED] Ability to scan into struct fields from multiple tables [#160](https://github.com/doug-martin/goqu/issues/160)

# v9.3.0

* [ADDED] Using Update, Insert, or Delete datasets in sub selects and CTEs [#164](https://github.com/doug-martin/goqu/issues/164)
Expand Down
16 changes: 8 additions & 8 deletions database_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,27 +109,27 @@ func ExampleDatabase_Dialect() {
func ExampleDatabase_Exec() {
db := getDb()

_, err := db.Exec(`DROP TABLE "goqu_user"`)
_, err := db.Exec(`DROP TABLE "user_role"; DROP TABLE "goqu_user"`)
if err != nil {
fmt.Println("Error occurred while dropping table", err.Error())
fmt.Println("Error occurred while dropping tables", err.Error())
}
fmt.Println("Dropped table goqu_user")
fmt.Println("Dropped tables user_role and goqu_user")
// Output:
// Dropped table goqu_user
// Dropped tables user_role and goqu_user
}

func ExampleDatabase_ExecContext() {
db := getDb()
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
_, err := db.ExecContext(ctx, `DROP TABLE "goqu_user"`)
_, err := db.ExecContext(ctx, `DROP TABLE "user_role"; DROP TABLE "goqu_user"`)
if err != nil {
fmt.Println("Error occurred while dropping table", err.Error())
fmt.Println("Error occurred while dropping tables", err.Error())
}
fmt.Println("Dropped table goqu_user")
fmt.Println("Dropped tables user_role and goqu_user")
// Output:
// Dropped table goqu_user
// Dropped tables user_role and goqu_user
}

func ExampleDatabase_From() {
Expand Down
150 changes: 150 additions & 0 deletions docs/selecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,78 @@ if err := db.From("user").Select("first_name").ScanStructs(&users); err != nil{
fmt.Printf("\n%+v", users)
```

`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.

**NOTE** When calling `ScanStructs` without a select already defined it will automatically only `SELECT` the columns found in the struct

```go
type Role struct {
Id uint64 `db:"id"`
UserID uint64 `db:"user_id"`
Name string `db:"name"`
}
type User struct {
Id uint64 `db:"id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
}
type UserAndRole struct {
User User `db:"goqu_user"` // tag as the "goqu_user" table
Role Role `db:"user_role"` // tag as "user_role" table
}
db := getDb()

ds := db.
From("goqu_user").
Join(goqu.T("user_role"), goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id"))))
var users []UserAndRole
// Scan structs will auto build the
if err := ds.ScanStructs(&users); err != nil {
fmt.Println(err.Error())
return
}
for _, u := range users {
fmt.Printf("\n%+v", u)
}
```

You can alternatively manually select the columns with the appropriate aliases using the `goqu.C` method to create the alias.

```go
type Role struct {
UserID uint64 `db:"user_id"`
Name string `db:"name"`
}
type User struct {
Id uint64 `db:"id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Role Role `db:"user_role"` // tag as "user_role" table
}
db := getDb()

ds := db.
Select(
"goqu_user.id",
"goqu_user.first_name",
"goqu_user.last_name",
// alias the fully qualified identifier `C` is important here so it doesnt parse it
goqu.I("user_role.user_id").As(goqu.C("user_role.user_id")),
goqu.I("user_role.name").As(goqu.C("user_role.name")),
).
From("goqu_user").
Join(goqu.T("user_role"), goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id"))))

var users []User
if err := ds.ScanStructs(&users); err != nil {
fmt.Println(err.Error())
return
}
for _, u := range users {
fmt.Printf("\n%+v", u)
}
```

<a name="scan-struct"></a>
**[`ScanStruct`](http://godoc.org/github.com/doug-martin/goqu#SelectDataset.ScanStruct)**

Expand Down Expand Up @@ -869,6 +941,83 @@ if !found {
}
```

`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.

**NOTE** When calling `ScanStruct` without a select already defined it will automatically only `SELECT` the columns found in the struct

```go
type Role struct {
UserID uint64 `db:"user_id"`
Name string `db:"name"`
}
type User struct {
ID uint64 `db:"id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
}
type UserAndRole struct {
User User `db:"goqu_user"` // tag as the "goqu_user" table
Role Role `db:"user_role"` // tag as "user_role" table
}
db := getDb()
var userAndRole UserAndRole
ds := db.
From("goqu_user").
Join(goqu.T("user_role"),goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id")))).
Where(goqu.C("first_name").Eq("Bob"))

found, err := ds.ScanStruct(&userAndRole)
if err != nil{
fmt.Println(err.Error())
return
}
if !found {
fmt.Println("No user found")
} else {
fmt.Printf("\nFound user: %+v", user)
}
```

You can alternatively manually select the columns with the appropriate aliases using the `goqu.C` method to create the alias.

```go
type Role struct {
UserID uint64 `db:"user_id"`
Name string `db:"name"`
}
type User struct {
ID uint64 `db:"id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Role Role `db:"user_role"` // tag as "user_role" table
}
db := getDb()
var userAndRole UserAndRole
ds := db.
Select(
"goqu_user.id",
"goqu_user.first_name",
"goqu_user.last_name",
// alias the fully qualified identifier `C` is important here so it doesnt parse it
goqu.I("user_role.user_id").As(goqu.C("user_role.user_id")),
goqu.I("user_role.name").As(goqu.C("user_role.name")),
).
From("goqu_user").
Join(goqu.T("user_role"),goqu.On(goqu.I("goqu_user.id").Eq(goqu.I("user_role.user_id")))).
Where(goqu.C("first_name").Eq("Bob"))

found, err := ds.ScanStruct(&userAndRole)
if err != nil{
fmt.Println(err.Error())
return
}
if !found {
fmt.Println("No user found")
} else {
fmt.Printf("\nFound user: %+v", user)
}
```


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

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




78 changes: 76 additions & 2 deletions exec/query_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -415,8 +416,8 @@ func (qes *queryExecutorSuite) TestScanStructs_withIgnoredEmbeddedPointerStruct(
var composed []ComposedIgnoredPointerStruct
qes.NoError(e.ScanStructs(&composed))
qes.Equal([]ComposedIgnoredPointerStruct{
{StructWithTags: &StructWithTags{}, PhoneNumber: testPhone1, Age: testAge1},
{StructWithTags: &StructWithTags{}, PhoneNumber: testPhone2, Age: testAge2},
{PhoneNumber: testPhone1, Age: testAge1},
{PhoneNumber: testPhone2, Age: testAge2},
}, composed)
}

Expand Down Expand Up @@ -944,6 +945,79 @@ func (qes *queryExecutorSuite) TestScanStruct() {
}, noTag)
}

func (qes *queryExecutorSuite) TestScanStruct_taggedStructs() {
type StructWithNoTags struct {
Address string
Name string
}

type StructWithTags struct {
Address string `db:"address"`
Name string `db:"name"`
}

type ComposedStruct struct {
StructWithTags
PhoneNumber string `db:"phone_number"`
Age int64 `db:"age"`
}
type ComposedWithPointerStruct struct {
*StructWithTags
PhoneNumber string `db:"phone_number"`
Age int64 `db:"age"`
}

type StructWithTaggedStructs struct {
NoTags StructWithNoTags `db:"notags"`
Tags StructWithTags `db:"tags"`
Composed ComposedStruct `db:"composedstruct"`
ComposedPointer ComposedWithPointerStruct `db:"composedptrstruct"`
}

db, mock, err := sqlmock.New()
qes.NoError(err)

cols := []string{
"notags.address", "notags.name",
"tags.address", "tags.name",
"composedstruct.address", "composedstruct.name", "composedstruct.phone_number", "composedstruct.age",
"composedptrstruct.address", "composedptrstruct.name", "composedptrstruct.phone_number", "composedptrstruct.age",
}

q := `SELECT` + strings.Join(cols, ", ") + ` FROM "items"`

mock.ExpectQuery(q).
WithArgs().
WillReturnRows(sqlmock.NewRows(cols).AddRow(
testAddr1, testName1,
testAddr2, testName2,
testAddr1, testName1, testPhone1, testAge1,
testAddr2, testName2, testPhone2, testAge2,
))

e := newQueryExecutor(db, nil, q)

var item StructWithTaggedStructs
found, err := e.ScanStruct(&item)
qes.NoError(err)
qes.True(found)
qes.Equal(StructWithTaggedStructs{
NoTags: StructWithNoTags{Address: testAddr1, Name: testName1},
Tags: StructWithTags{Address: testAddr2, Name: testName2},
Composed: ComposedStruct{
StructWithTags: StructWithTags{Address: testAddr1, Name: testName1},
PhoneNumber: testPhone1,
Age: testAge1,
},
ComposedPointer: ComposedWithPointerStruct{
StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2},
PhoneNumber: testPhone2,
Age: testAge2,
},
}, item)

}

func (qes *queryExecutorSuite) TestScanVals() {
db, mock, err := sqlmock.New()
qes.NoError(err)
Expand Down
7 changes: 6 additions & 1 deletion exp/col.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ func NewColumnListExpression(vals ...interface{}) ColumnListExpression {
}
structCols := cm.Cols()
for _, col := range structCols {
cols = append(cols, ParseIdentifier(col))
i := ParseIdentifier(col)
var sc Expression = i
if i.IsQualified() {
sc = i.As(NewIdentifierExpression("", "", col))
}
cols = append(cols, sc)
}
} else {
panic(fmt.Sprintf("Cannot created expression from %+v", val))
Expand Down
8 changes: 8 additions & 0 deletions exp/exp.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ type (
Updateable
Distinctable
Castable
// returns true if this identifier has more more than on part (Schema, Table or Col)
// "schema" -> true //cant qualify anymore
// "schema.table" -> true
// "table" -> false
// "schema"."table"."col" -> true
// "table"."col" -> true
// "col" -> false
IsQualified() bool
// Returns a new IdentifierExpression with the specified schema
Schema(string) IdentifierExpression
// Returns the current schema
Expand Down
18 changes: 18 additions & 0 deletions exp/ident.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ func (i identifier) Clone() Expression {
return i.clone()
}

func (i identifier) IsQualified() bool {
schema, table, col := i.schema, i.table, i.col
switch c := col.(type) {
case string:
if c != "" {
return len(table) > 0 || len(schema) > 0
}
default:
if c != nil {
return len(table) > 0 || len(schema) > 0
}
}
if len(table) > 0 {
return len(schema) > 0
}
return false
}

// Sets the table on the current identifier
// I("col").Table("table") -> "table"."col" //postgres
// I("col").Table("table") -> `table`.`col` //mysql
Expand Down
Loading

0 comments on commit cddc40e

Please sign in to comment.