package sq import ( "errors" "fmt" "github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx/reflectx" "gogs.mikescher.com/BlackForestBytes/goext/langext" "reflect" "strings" ) // 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 converter []ssConverter columns []string } func NewStructScanner(rows *sqlx.Rows, unsafe bool) *StructScanner { return &StructScanner{ rows: rows, Mapper: reflectx.NewMapper("db"), unsafe: unsafe, } } type ssConverter struct { Converter DBTypeConverter RefCount int } 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)) r.converter = make([]ssConverter, len(columns)) return nil } // StructScanExt forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go // does also work with nullabel structs (from LEFT JOIN's) // does also work with custom value converters func (r *StructScanner) StructScanExt(q Queryable, 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(q, v, r.fields, r.values, r.converter) 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() 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 { if r.converter[i].Converter != nil { val3 := val2.Elem() conv3, err := r.converter[i].Converter.DBToModel(val3.Interface()) if err != nil { return err } conv3RVal := reflect.ValueOf(conv3) for j := 0; j < r.converter[i].RefCount; j++ { newConv3Val := reflect.New(conv3RVal.Type()) newConv3Val.Elem().Set(conv3RVal) conv3RVal = newConv3Val } f.Set(conv3RVal) } else { f.Set(val2.Elem()) } } } 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(q Queryable, v reflect.Value, traversals [][]int, values []interface{}, converter []ssConverter) 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) typeStr := f.Type().String() foundConverter := false for _, conv := range q.ListConverter() { if conv.ModelTypeString() == typeStr { _v := langext.Ptr[any](nil) values[i] = _v foundConverter = true converter[i] = ssConverter{Converter: conv, RefCount: 0} break } } if !foundConverter { // also allow non-pointer converter for pointer-types for _, conv := range q.ListConverter() { if conv.ModelTypeString() == strings.TrimLeft(typeStr, "*") { _v := langext.Ptr[any](nil) values[i] = _v foundConverter = true converter[i] = ssConverter{Converter: conv, RefCount: len(typeStr) - len(strings.TrimLeft(typeStr, "*"))} // kind hacky way to get the amount of ptr before , but it works... break } } } if !foundConverter { values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface() converter[i] = ssConverter{Converter: nil, RefCount: -1} } } 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 }