Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 73 additions & 62 deletions sqlstruct.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,85 @@ the Go standard library's database/sql package.
The package matches struct field names to SQL query column names. A field can
also specify a matching column with "sql" tag, if it's different from field
name. Unexported fields or fields marked with `sql:"-"` are ignored, just like
with "encoding/json" package.
with "encoding/json" package. Fields marked with `sql:",recurse"` are treated as
embedded structs and are recursively scanned.

For example:

type T struct {
F1 string
F2 string `sql:"field2"`
F3 string `sql:"-"`
}
type T1 struct {
F5 string `sql:"field5"`
}

type T struct {
F1 string
F2 string `sql:"field2"`
F3 string `sql:"-"`
F4 T1 `sql:",recurse"`
}

rows, err := db.Query(fmt.Sprintf("SELECT %s FROM tablename", sqlstruct.Columns(T{})))
...
rows, err := db.Query(fmt.Sprintf("SELECT %s FROM tablename", sqlstruct.Columns(T{})))
...

for rows.Next() {
var t T
err = sqlstruct.Scan(&t, rows)
...
}
for rows.Next() {
var t T
err = sqlstruct.Scan(&t, rows)
...
}

err = rows.Err() // get any errors encountered during iteration
err = rows.Err() // get any errors encountered during iteration

Aliased tables in a SQL statement may be scanned into a specific structure identified
by the same alias, using the ColumnsAliased and ScanAliased functions:

type User struct {
Id int `sql:"id"`
Username string `sql:"username"`
Email string `sql:"address"`
Name string `sql:"name"`
HomeAddress *Address `sql:"-"`
}

type Address struct {
Id int `sql:"id"`
City string `sql:"city"`
Street string `sql:"address"`
}

...

var user User
var address Address
sql := `
SELECT %s, %s FROM users AS u
INNER JOIN address AS a ON a.id = u.address_id
WHERE u.username = ?
`
sql = fmt.Sprintf(sql, sqlstruct.ColumnsAliased(*user, "u"), sqlstruct.ColumnsAliased(*address, "a"))
rows, err := db.Query(sql, "gedi")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
if rows.Next() {
err = sqlstruct.ScanAliased(&user, rows, "u")
if err != nil {
log.Fatal(err)
}
err = sqlstruct.ScanAliased(&address, rows, "a")
if err != nil {
log.Fatal(err)
}
user.HomeAddress = address
}
fmt.Printf("%+v", *user)
// output: "{Id:1 Username:gedi Email:gediminas.morkevicius@gmail.com Name:Gedas HomeAddress:0xc21001f570}"
fmt.Printf("%+v", *user.HomeAddress)
// output: "{Id:2 City:Vilnius Street:Plento 34}"
type Metadata struct {
CreatedAt time.Time `sql:"created_at"`
}

type User struct {
Id int `sql:"id"`
Username string `sql:"username"`
Email string `sql:"address"`
Name string `sql:"name"`
HomeAddress *Address `sql:"-"`
Metadata Metadata `sql:",recurse"`
}

type Address struct {
Id int `sql:"id"`
City string `sql:"city"`
Street string `sql:"address"`
}

...

var user User
var address Address
sql := `
SELECT %s, %s FROM users AS u
INNER JOIN address AS a ON a.id = u.address_id
WHERE u.username = ?
`
sql = fmt.Sprintf(sql, sqlstruct.ColumnsAliased(*user, "u"), sqlstruct.ColumnsAliased(*address, "a"))
rows, err := db.Query(sql, "gedi")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
if rows.Next() {
err = sqlstruct.ScanAliased(&user, rows, "u")
if err != nil {
log.Fatal(err)
}
err = sqlstruct.ScanAliased(&address, rows, "a")
if err != nil {
log.Fatal(err)
}
user.HomeAddress = address
}
fmt.Printf("%+v", *user)
// output: "{Id:1 Username:gedi Email:gediminas.morkevicius@gmail.com Name:Gedas HomeAddress:0xc21001f570}"
fmt.Printf("%+v", *user.HomeAddress)
// output: "{Id:2 City:Vilnius Street:Plento 34}"
*/
package sqlstruct

Expand All @@ -97,7 +107,7 @@ import (
// The default mapper converts field names to lower case. If instead you would prefer
// field names converted to snake case, simply assign sqlstruct.ToSnakeCase to the variable:
//
// sqlstruct.NameMapper = sqlstruct.ToSnakeCase
// sqlstruct.NameMapper = sqlstruct.ToSnakeCase
//
// Alternatively for a custom mapping, any func(string) string can be used instead.
var NameMapper func(string) string = strings.ToLower
Expand Down Expand Up @@ -145,8 +155,8 @@ func getFieldInfo(typ reflect.Type) fieldInfo {
continue
}

// Handle embedded structs
if f.Anonymous && f.Type.Kind() == reflect.Struct {
// Handle embedded and recurse tagged structs
if (f.Anonymous || strings.EqualFold(tag, ",recurse")) && f.Type.Kind() == reflect.Struct {
for k, v := range getFieldInfo(f.Type) {
finfo[k] = append([]int{i}, v...)
}
Expand Down Expand Up @@ -198,7 +208,8 @@ func Columns(s interface{}) string {
// given alias.
//
// For each field in the given struct it will generate a statement like:
// alias.field AS alias_field
//
// alias.field AS alias_field
//
// It is intended to be used in conjunction with the ScanAliased function.
func ColumnsAliased(s interface{}, alias string) string {
Expand Down
15 changes: 15 additions & 0 deletions sqlstruct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ type testType2 struct {
FieldSec string `sql:"field_sec"`
}

type testType3 struct {
FieldA string `sql:"field_a"`
EmbeddedType EmbeddedType `sql:",recurse"`
}

// testRows is a mock version of sql.Rows which can only scan strings
type testRows struct {
columns []string
Expand Down Expand Up @@ -67,6 +72,16 @@ func TestColumns(t *testing.T) {
}
}

func TestColumnDeep(t *testing.T) {
var v testType3
e := "field_a, field_e"
c := Columns(v)

if c != e {
t.Errorf("expected %q got %q", e, c)
}
}

func TestColumnsAliased(t *testing.T) {
var t1 testType
var t2 testType2
Expand Down