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 }