goext/sq/structscanner.go
2022-12-23 19:11:18 +01:00

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
}