Skip to content

Commit

Permalink
Improved struct type recursion with DeepFields
Browse files Browse the repository at this point in the history
  • Loading branch information
aodin committed May 29, 2016
1 parent f08c8c8 commit 562b58c
Show file tree
Hide file tree
Showing 17 changed files with 383 additions and 407 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A SQL toolkit for Go - in the style of Python's [SQLAlchemy Core](http://docs.sq
* Build complete database schemas
* Create reusable and cross-dialect SQL statements
* Allow struct instances and slices to be directly populated by the database
* Support for MySQL, PostGres, and SQLite3

Quickstart
----------
Expand Down Expand Up @@ -103,7 +104,7 @@ if err = conn.Query(Users.Select(), &user); err != nil {
If you'd prefer to have queries panic on error, you can create a panicky version of the connection:

```go
panicky := conn.PanicOnError()
panicky := conn.PanicOnError() // or Must()
panicky.Query(Users.Create())
```

Expand Down
123 changes: 86 additions & 37 deletions columnset.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import (
)

// ColumnSet maintains a []ColumnElem. It includes a variety of
// getters and setter. Optionally, it can force unique
// getters, setters, and filters.
type ColumnSet struct {
// TODO what about uniqueness with aliases? across tables?
unique bool
order []ColumnElem
order []ColumnElem
}

func (set ColumnSet) Compile(d dialect.Dialect, ps *Parameters) (string, error) {
Expand All @@ -31,33 +29,9 @@ func (set ColumnSet) Compile(d dialect.Dialect, ps *Parameters) (string, error)
}

// Add adds any number of Columnar types to the set and returns the new set.
// If the set is marked unique, adding a column with the same name
// as an existing column in the set will return an error.
func (set ColumnSet) Add(columns ...Columnar) (ColumnSet, error) {
if set.unique {
for _, column := range columns {
for _, existing := range set.order {
// TODO across tables? aliases?
if existing.Name() == column.Name() {
if existing.Table() == nil {
return set, fmt.Errorf(
"sol: this set already has a column named '%s'",
existing.Name(),
)
}
return set, fmt.Errorf(
"sol: table '%s' already has a column named '%s'",
existing.Table().Name(),
existing.Name(),
)
}
}
set.order = append(set.order, column.Column())
}
} else {
for _, column := range columns {
set.order = append(set.order, column.Column())
}
for _, column := range columns {
set.order = append(set.order, column.Column())
}
return set, nil
}
Expand All @@ -72,11 +46,33 @@ func (set ColumnSet) Exists() bool {
return len(set.order) > 0
}

// Filters returns a ColumnSet of columns from the original set that
// match the given names
func (set ColumnSet) Filter(names ...string) (out ColumnSet) {
for _, column := range set.order {
for _, name := range names {
if column.Name() == name {
out.order = append(out.order, column)
break
}
}
}
return
}

// FullNames returns the full names of the set's columns without alias
func (set ColumnSet) FullNames() []string {
names := make([]string, len(set.order))
for i, col := range set.order {
names[i] = col.FullName()
}
return names
}

// Get returns a ColumnElem - or an invalid ColumnElem if a column
// with the given name does not exist in the set
func (set ColumnSet) Get(name string) ColumnElem {
for _, column := range set.order {
// TODO What about table? aliases?
if column.Name() == name {
return column
}
Expand All @@ -94,22 +90,75 @@ func (set ColumnSet) IsEmpty() bool {
return len(set.order) == 0
}

// Names returns the full names of the set's columns without alias
// Names returns the names of the set's columns without alias
func (set ColumnSet) Names() []string {
names := make([]string, len(set.order))
for i, col := range set.order {
names[i] = col.FullName()
names[i] = col.Name()
}
return names
}

// UniqueColumns creates a new ColumnSet that can only hold columns
// with unique names
func UniqueColumns() ColumnSet {
return ColumnSet{unique: true}
// Reject removes the given column names and returns a new ColumnSet
func (set ColumnSet) Reject(names ...string) (out ColumnSet) {
ColumnLoop:
for _, column := range set.order {
for _, name := range names {
if column.Name() == name {
continue ColumnLoop
}
}
out.order = append(out.order, column)
}
return
}

// Columns creates a new ColumnSet
func Columns(columns ...ColumnElem) ColumnSet {
return ColumnSet{order: columns}
}

// UniqueColumnSet is a ColumnSet, but column names must be unique
type UniqueColumnSet struct{ ColumnSet }

// Add adds any number of Columnar types to the set and returns the new set.
// Adding a column with the same name as an existing column in the set
// will return an error.
func (set UniqueColumnSet) Add(columns ...Columnar) (UniqueColumnSet, error) {
for _, column := range columns {
for _, existing := range set.order {
if existing.Name() == column.Name() {
if existing.Table() == nil {
return set, fmt.Errorf(
"sol: this set already has a column named '%s'",
existing.Name(),
)
}
return set, fmt.Errorf(
"sol: table '%s' already has a column named '%s'",
existing.Table().Name(),
existing.Name(),
)
}
}
set.order = append(set.order, column.Column())
}
return set, nil
}

// Filters returns a UniqueColumnSet of columns from the original
// set that match the given names
func (set UniqueColumnSet) Filter(names ...string) (out UniqueColumnSet) {
return UniqueColumnSet{ColumnSet: set.ColumnSet.Filter(names...)}
}

// Reject removes the given column names and returns a new ColumnSet
func (set UniqueColumnSet) Reject(names ...string) (out UniqueColumnSet) {
return UniqueColumnSet{ColumnSet: set.ColumnSet.Reject(names...)}
}

// UniqueColumns creates a new ColumnSet that can only hold columns
// with unique names
func UniqueColumns() (set UniqueColumnSet) {
return
}
7 changes: 7 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package sol

import (
"errors"
"fmt"
"strings"
)

// ErrNoColumns is returned when attempting to compile a query without
// any columns
var ErrNoColumns = errors.New(
"sol: cannot compile a statement without columns",
)

type fieldError struct {
column string
table string
Expand Down
61 changes: 58 additions & 3 deletions fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sol

import (
"reflect"
"time"
"unicode"

"database/sql"
Expand All @@ -14,6 +15,61 @@ const (

var scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()

// Field holds value and type info on a struct field
type Field struct {
Value reflect.Value
Type reflect.StructField
}

// DeepFields returns value and type info on struct types
func DeepFields(obj interface{}) (fields []Field) {
val := reflect.ValueOf(obj)
typ := reflect.TypeOf(obj)
if typ != nil && typ.Kind() == reflect.Ptr {
typ = val.Elem().Type()
val = reflect.Indirect(val)
}
if typ == nil || typ.Kind() != reflect.Struct {
return // Do not iterate on non-struct types
}

for i := 0; i < typ.NumField(); i++ {
field := Field{Value: val.Field(i), Type: typ.Field(i)}

// If the field has an ignore tag, skip it and any descendants
if field.Type.Tag.Get(tagLabel) == ignoreTag {
continue
}

// Skip fields that cannot be interfaced
if !field.Value.CanInterface() {
continue
}

// Time structs have special handling
switch field.Value.Interface().(type) {
case time.Time, *time.Time:
fields = append(fields, field)
continue
}

// Scanners have special handling
if reflect.PtrTo(field.Type.Type).Implements(scannerType) {
fields = append(fields, field)
continue
}

// Save the field or recurse further
switch field.Value.Kind() {
case reflect.Struct:
fields = append(fields, DeepFields(field.Value.Interface())...)
default:
fields = append(fields, field)
}
}
return
}

type field struct {
names []string // struct field names - with possible embedding
column string // SQL column name
Expand Down Expand Up @@ -142,8 +198,7 @@ func SelectFields(v interface{}) fields {
return recurse([]string{}, reflect.TypeOf(v).Elem())
}

// SelectFieldsFromElem returns the ordered list of fields from the given
// reflect Type
func SelectFieldsFromElem(elem reflect.Type) fields {
// FieldsFromElem returns the list of fields from the given reflect.Type
func FieldsFromElem(elem reflect.Type) fields {
return recurse([]string{}, elem)
}
4 changes: 1 addition & 3 deletions fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ func TestSelectFields_ignored(t *testing.T) {
)
}

// TODO implement scanner

type embedded struct {
embeddedID
Name string
Expand All @@ -93,5 +91,5 @@ type nested struct {

type moreNesting struct {
nested
OneMore string
OneMore string `db:"text,omitempty"`
}
Loading

0 comments on commit 562b58c

Please sign in to comment.