224 lines
5.0 KiB
Go
224 lines
5.0 KiB
Go
package sq
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/jmoiron/sqlx/reflectx"
|
|
"reflect"
|
|
)
|
|
|
|
// forked from sqlx, but added ability to unmarshal optional-nested structs
|
|
|
|
type StructScanner struct {
|
|
rows *sqlx.Rows
|
|
Mapper *reflectx.Mapper
|
|
unsafe bool
|
|
|
|
fields [][]int
|
|
values []any
|
|
columns []string
|
|
}
|
|
|
|
func NewStructScanner(rows *sqlx.Rows, unsafe bool) *StructScanner {
|
|
return &StructScanner{
|
|
rows: rows,
|
|
Mapper: reflectx.NewMapper("db"),
|
|
unsafe: unsafe,
|
|
}
|
|
}
|
|
|
|
func (r *StructScanner) Start(dest any) error {
|
|
v := reflect.ValueOf(dest)
|
|
|
|
if v.Kind() != reflect.Ptr {
|
|
return errors.New("must pass a pointer, not a value, to StructScan destination")
|
|
}
|
|
|
|
columns, err := r.rows.Columns()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.columns = columns
|
|
r.fields = r.Mapper.TraversalsByName(v.Type(), columns)
|
|
// if we are not unsafe and are missing fields, return an error
|
|
if f, err := missingFields(r.fields); err != nil && !r.unsafe {
|
|
return fmt.Errorf("missing destination name %s in %T", columns[f], dest)
|
|
}
|
|
r.values = make([]interface{}, len(columns))
|
|
|
|
return nil
|
|
}
|
|
|
|
// StructScanExt forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
|
|
// does also wok with nullabel structs (from LEFT JOIN's)
|
|
func (r *StructScanner) StructScanExt(dest any) error {
|
|
v := reflect.ValueOf(dest)
|
|
|
|
if v.Kind() != reflect.Ptr {
|
|
return errors.New("must pass a pointer, not a value, to StructScan destination")
|
|
}
|
|
|
|
// ========= STEP 1 :: =========
|
|
|
|
v = v.Elem()
|
|
|
|
err := fieldsByTraversalExtended(v, r.fields, r.values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// scan into the struct field pointers and append to our results
|
|
err = r.rows.Scan(r.values...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nullStructs := make(map[string]bool)
|
|
|
|
for i, traversal := range r.fields {
|
|
if len(traversal) == 0 {
|
|
continue
|
|
}
|
|
|
|
isnsil := reflect.ValueOf(r.values[i]).Elem().IsNil()
|
|
|
|
for i := 1; i < len(traversal); i++ {
|
|
|
|
canParentNil := reflectx.FieldByIndexes(v, traversal[0:i]).Kind() == reflect.Pointer
|
|
|
|
k := fmt.Sprintf("%v", traversal[0:i])
|
|
if v, ok := nullStructs[k]; ok {
|
|
|
|
nullStructs[k] = canParentNil && v && isnsil
|
|
|
|
} else {
|
|
nullStructs[k] = canParentNil && isnsil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
forcenulled := make(map[string]bool)
|
|
|
|
for i, traversal := range r.fields {
|
|
if len(traversal) == 0 {
|
|
continue
|
|
}
|
|
|
|
anyparentnull := false
|
|
for i := 1; i < len(traversal); i++ {
|
|
k := fmt.Sprintf("%v", traversal[0:i])
|
|
if nv, ok := nullStructs[k]; ok && nv {
|
|
|
|
if _, ok := forcenulled[k]; !ok {
|
|
f := reflectx.FieldByIndexes(v, traversal[0:i])
|
|
f.Set(reflect.Zero(f.Type())) // set to nil
|
|
forcenulled[k] = true
|
|
}
|
|
|
|
anyparentnull = true
|
|
break
|
|
|
|
}
|
|
}
|
|
|
|
if anyparentnull {
|
|
continue
|
|
}
|
|
|
|
f := reflectx.FieldByIndexes(v, traversal)
|
|
|
|
val1 := reflect.ValueOf(r.values[i])
|
|
val2 := val1.Elem()
|
|
val3 := val2.Elem()
|
|
|
|
if val2.IsNil() {
|
|
if f.Kind() != reflect.Pointer {
|
|
return errors.New(fmt.Sprintf("Cannot set field %v to NULL value from column '%s' (type: %s)", traversal, r.columns[i], f.Type().String()))
|
|
}
|
|
|
|
f.Set(reflect.Zero(f.Type())) // set to nil
|
|
} else {
|
|
f.Set(val3)
|
|
}
|
|
|
|
}
|
|
|
|
return r.rows.Err()
|
|
}
|
|
|
|
// StructScanBase forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
|
|
// without (relevant) changes
|
|
func (r *StructScanner) StructScanBase(dest any) error {
|
|
v := reflect.ValueOf(dest)
|
|
|
|
if v.Kind() != reflect.Ptr {
|
|
return errors.New("must pass a pointer, not a value, to StructScan destination")
|
|
}
|
|
|
|
v = v.Elem()
|
|
|
|
err := fieldsByTraversalBase(v, r.fields, r.values, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// scan into the struct field pointers and append to our results
|
|
err = r.rows.Scan(r.values...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return r.rows.Err()
|
|
}
|
|
|
|
// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
|
|
func fieldsByTraversalExtended(v reflect.Value, traversals [][]int, values []interface{}) error {
|
|
v = reflect.Indirect(v)
|
|
if v.Kind() != reflect.Struct {
|
|
return errors.New("argument not a struct")
|
|
}
|
|
|
|
for i, traversal := range traversals {
|
|
if len(traversal) == 0 {
|
|
values[i] = new(interface{})
|
|
continue
|
|
}
|
|
f := reflectx.FieldByIndexes(v, traversal)
|
|
|
|
values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
|
|
func fieldsByTraversalBase(v reflect.Value, traversals [][]int, values []interface{}, ptrs bool) error {
|
|
v = reflect.Indirect(v)
|
|
if v.Kind() != reflect.Struct {
|
|
return errors.New("argument not a struct")
|
|
}
|
|
|
|
for i, traversal := range traversals {
|
|
if len(traversal) == 0 {
|
|
values[i] = new(interface{})
|
|
continue
|
|
}
|
|
f := reflectx.FieldByIndexes(v, traversal)
|
|
if ptrs {
|
|
values[i] = f.Addr().Interface()
|
|
} else {
|
|
values[i] = f.Interface()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// missingFields forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
|
|
func missingFields(transversals [][]int) (field int, err error) {
|
|
for i, t := range transversals {
|
|
if len(t) == 0 {
|
|
return i, errors.New("missing field")
|
|
}
|
|
}
|
|
return 0, nil
|
|
}
|