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 } // StructScan 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 := fieldsByTraversal(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.New(f.Type().Elem())) // 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.New(f.Type().Elem())) // set to nil } else { f.Set(val3) } } return r.rows.Err() } // StructScan 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 := fieldsByTraversal(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 } return r.rows.Err() } // fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go func fieldsByTraversal(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 } // 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 }