fix(09): WR-16 resolve model columns through embedded structs and explicit column tags
This commit is contained in:
98
modules/cabana/model_fields.go
Normal file
98
modules/cabana/model_fields.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// modelField is an exported struct field of a model together with its index
|
||||
// path from the model type, so a field promoted from an embedded struct
|
||||
// (gorm.Model, a shared timestamps struct) is reachable like a top-level one.
|
||||
type modelField struct {
|
||||
Field reflect.StructField
|
||||
Path []int
|
||||
}
|
||||
|
||||
var modelFieldCache sync.Map // reflect.Type -> []modelField
|
||||
|
||||
// modelFields lists the exported fields of the struct type behind t in
|
||||
// declaration order, with anonymous (embedded) structs flattened in place.
|
||||
// Embedded scalar-like structs (time.Time, sql.Null*, anything that is a
|
||||
// Scanner or Valuer) stay single fields. It returns nil for a non-struct.
|
||||
func modelFields(t reflect.Type) []modelField {
|
||||
for t != nil && t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t == nil || t.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
if cached, ok := modelFieldCache.Load(t); ok {
|
||||
return cached.([]modelField)
|
||||
}
|
||||
fields := flattenModelFields(t, nil, 0)
|
||||
modelFieldCache.Store(t, fields)
|
||||
return fields
|
||||
}
|
||||
|
||||
func flattenModelFields(t reflect.Type, prefix []int, depth int) []modelField {
|
||||
var out []modelField
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
path := append(append([]int(nil), prefix...), i)
|
||||
if f.Anonymous && depth < 5 {
|
||||
if inner := embeddedStructType(f.Type); inner != nil {
|
||||
out = append(out, flattenModelFields(inner, path, depth+1)...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, modelField{Field: f, Path: path})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var (
|
||||
scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
|
||||
valuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
)
|
||||
|
||||
// embeddedStructType returns the struct type to flatten for an embedded field,
|
||||
// or nil when the field is a single value.
|
||||
func embeddedStructType(t reflect.Type) reflect.Type {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct || t == timeType {
|
||||
return nil
|
||||
}
|
||||
ptr := reflect.PointerTo(t)
|
||||
if ptr.Implements(scannerType) || t.Implements(valuerType) || ptr.Implements(valuerType) {
|
||||
return nil
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// hasPrimaryKeyTag reports whether a field's gorm tag declares it the primary
|
||||
// key. GORM reads the key case-insensitively (gorm.Model writes "primarykey").
|
||||
func hasPrimaryKeyTag(field reflect.StructField) bool {
|
||||
for _, part := range strings.Split(field.Tag.Get("gorm"), ";") {
|
||||
if strings.EqualFold(strings.TrimSpace(part), "primarykey") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// defaultColumnName is GORM's default column for an untagged field.
|
||||
func defaultColumnName(field reflect.StructField) string {
|
||||
return schema.NamingStrategy{}.ColumnName("", field.Name)
|
||||
}
|
||||
Reference in New Issue
Block a user