github.com/mitranim/gg@v0.1.17/gsql/gsql_internal.go (about)

     1  package gsql
     2  
     3  import (
     4  	"database/sql"
     5  	r "reflect"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/mitranim/gg"
    10  )
    11  
    12  func popSqlArrSegment(ptr *string) string {
    13  	var lvl int
    14  	src := *ptr
    15  
    16  	for ind, char := range src {
    17  		if char == '{' {
    18  			lvl++
    19  			continue
    20  		}
    21  
    22  		if char == '}' {
    23  			lvl--
    24  			if lvl < 0 {
    25  				panic(gg.ErrInvalidInput)
    26  			}
    27  			continue
    28  		}
    29  
    30  		if char == ',' && lvl == 0 {
    31  			*ptr = src[ind+1:]
    32  			return src[:ind]
    33  		}
    34  	}
    35  
    36  	*ptr = ``
    37  	return src
    38  }
    39  
    40  func typeReferenceField(typ r.Type) (_ r.StructField, _ bool) {
    41  	if typ.Kind() == r.Struct && typ.NumField() > 0 {
    42  		field := gg.StructFieldCache.Get(typ)[0]
    43  		if field.Tag.Get(`role`) == `ref` {
    44  			return field, true
    45  		}
    46  	}
    47  	return
    48  }
    49  
    50  func isTypeNonScannableStruct(typ r.Type) bool {
    51  	return typ.Kind() == r.Struct &&
    52  		!typ.ConvertibleTo(gg.Type[time.Time]()) &&
    53  		!typ.Implements(gg.Type[gg.Scanner]()) &&
    54  		!r.PointerTo(typ).Implements(gg.Type[gg.Scanner]())
    55  }
    56  
    57  var typeMetaCache = gg.TypeCacheOf[typeMeta]()
    58  
    59  type typeMeta map[string][]int
    60  
    61  func (self typeMeta) Get(key string) []int {
    62  	val, ok := self[key]
    63  	if !ok {
    64  		panic(gg.Errf(`unknown column %q`, key))
    65  	}
    66  	return val
    67  }
    68  
    69  func (self typeMeta) IsScalar() bool { return self == nil }
    70  
    71  // Called by `TypeCache`.
    72  func (self *typeMeta) Init(typ r.Type) { self.addAny(nil, nil, typ) }
    73  
    74  func (self *typeMeta) initMap() typeMeta { return gg.MapInit(self) }
    75  
    76  func (self *typeMeta) addAny(index []int, cols []string, typ r.Type) {
    77  	field, ok := typeReferenceField(typ)
    78  	if ok {
    79  		self.addAny(gg.Concat(index, field.Index), cols, field.Type)
    80  	}
    81  
    82  	if isTypeNonScannableStruct(typ) {
    83  		self.addStruct(index, cols, typ)
    84  		return
    85  	}
    86  
    87  	if len(cols) > 0 {
    88  		self.initMap()[strings.Join(cols, `.`)] = index
    89  	}
    90  }
    91  
    92  func (self *typeMeta) addStruct(index []int, cols []string, typ r.Type) {
    93  	self.initMap()
    94  	for _, field := range gg.StructPublicFieldCache.Get(typ) {
    95  		self.addField(index, cols, field)
    96  	}
    97  }
    98  
    99  func (self *typeMeta) addField(index []int, cols []string, field r.StructField) {
   100  	col := gg.FieldDbName(field)
   101  	typ := field.Type
   102  
   103  	if col != `` {
   104  		self.addAny(gg.Concat(index, field.Index), gg.CloneAppend(cols, col), typ)
   105  		return
   106  	}
   107  
   108  	if !field.Anonymous {
   109  		return
   110  	}
   111  
   112  	if typ.Kind() == r.Struct {
   113  		self.addStruct(gg.Concat(index, field.Index), cols, typ)
   114  		return
   115  	}
   116  
   117  	panic(gg.Errf(
   118  		`unsupported embedded type %q; embedded fields must be structs`,
   119  		typ,
   120  	))
   121  }
   122  
   123  func scanNextScalar[Row any, Src ColumnerScanner](src Src) (out Row) {
   124  	gg.Try(src.Scan(gg.AnyNoEscUnsafe(&out)))
   125  	return
   126  }
   127  
   128  func scanNextStruct[Row any, Src ColumnerScanner](src Src) (out Row) {
   129  	scanStructReflect(src, r.ValueOf(gg.AnyNoEscUnsafe(&out)).Elem())
   130  	return
   131  }
   132  
   133  /*
   134  TODO needs performance tuning.
   135  
   136  Would be nice to use an implementation similar to this:
   137  
   138  	gg.Try(src.Scan(gg.Map(RowsCols(src), func(key string) any {
   139  		return tar.FieldByIndex(meta.Get(key)).Addr().Interface()
   140  	})...))
   141  
   142  ...But the SQL driver doesn't allow to decode SQL "null" into non-nullable
   143  destinations such as `string` fields. This behavior is inconsistent with
   144  JSON, and unfortunate for our purposes.
   145  */
   146  func scanStructReflect[Src ColumnerScanner](src Src, tar r.Value) {
   147  	typ := tar.Type()
   148  	meta := typeMetaCache.Get(typ)
   149  	cols := gg.Try1(src.Columns())
   150  	indir := gg.Map(cols, func(key string) r.Value {
   151  		return r.New(r.PointerTo(typ.FieldByIndex(meta.Get(key)).Type))
   152  	})
   153  
   154  	gg.Try(src.Scan(gg.Map(indir, r.Value.Interface)...))
   155  
   156  	gg.Each2(cols, indir, func(key string, val r.Value) {
   157  		val = val.Elem()
   158  		if !val.IsNil() {
   159  			tar.FieldByIndex(meta.Get(key)).Set(val.Elem())
   160  		}
   161  	})
   162  }
   163  
   164  func scanValsReflect[Src Rows](src Src, tar r.Value) {
   165  	defer gg.Close(src)
   166  
   167  	for src.Next() {
   168  		const off = 1
   169  
   170  		// Increase length by one, effectively appending a zero value to the slice.
   171  		// Similar to `r.Append(r.New(typ).Elem())`, but should be marginally more
   172  		// efficient.
   173  		ind := tar.Len()
   174  		tar.Grow(off)
   175  		tar.SetLen(ind + off)
   176  
   177  		// Settable, addressable reference to the newly appended zero value.
   178  		out := tar.Index(ind)
   179  
   180  		// Hide new value from consumer code until scan is successful.
   181  		tar.SetLen(ind)
   182  
   183  		scanReflect(src, out)
   184  
   185  		// After successful scan, we can reveal new element to consumer code.
   186  		tar.SetLen(ind + off)
   187  	}
   188  
   189  	gg.ErrOk(src)
   190  }
   191  
   192  func scanValReflect[Src Rows](src Src, tar r.Value) {
   193  	defer gg.Close(src)
   194  
   195  	if !src.Next() {
   196  		panic(gg.AnyErrTraced(sql.ErrNoRows))
   197  	}
   198  
   199  	scanReflect(src, tar)
   200  	gg.ErrOk(src)
   201  
   202  	if src.Next() {
   203  		panic(gg.AnyErrTraced(ErrMultipleRows))
   204  	}
   205  }
   206  
   207  func scanReflect[Src ColumnerScanner](src Src, tar r.Value) {
   208  	if isValueScalar(tar) {
   209  		scanScalarReflect(src, tar)
   210  		return
   211  	}
   212  	scanStructReflect(src, tar)
   213  }
   214  
   215  func scanScalarReflect[Src ColumnerScanner](src Src, tar r.Value) {
   216  	gg.Try(src.Scan(tar.Addr().Interface()))
   217  }
   218  
   219  func isScalar[A any]() bool {
   220  	return typeMetaCache.Get(gg.Type[A]()).IsScalar()
   221  }
   222  
   223  func isValueScalar(val r.Value) bool {
   224  	return typeMetaCache.Get(val.Type()).IsScalar()
   225  }