From b1654616a6e94917356fb7db6a4c8c0e30652051 Mon Sep 17 00:00:00 2001 From: vc42 Date: Sun, 22 Mar 2026 19:43:57 -0400 Subject: [PATCH] fixed view/codec.go issues --- view/codec.go | 247 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 215 insertions(+), 32 deletions(-) diff --git a/view/codec.go b/view/codec.go index 2b9e331c..f210b085 100644 --- a/view/codec.go +++ b/view/codec.go @@ -3,54 +3,79 @@ package view import ( "context" "fmt" + "reflect" + "strconv" + "strings" + "unicode" + codec2 "github.com/viant/datly/view/extension/codec" "github.com/viant/sqlx/io" "github.com/viant/structology" "github.com/viant/xdatly/codec" "github.com/viant/xunsafe" - "reflect" - "strconv" - "strings" ) const ( rawFieldName = "Raw" + shadowFieldName = "Shadow" actualFieldName = "Actual" ) type ( + // columnsCodec builds a wrapper type: + // - Raw: embedded, holds promoted Col{i} with sqlx tags for DB scan of codec sources + // - Shadow: embedded, mirrors Actual's exported fields+tags so sqlx can scan non-codec cols + // - Actual: named (non-embedded) model type to avoid reflect panic when it has methods columnsCodec struct { - fields []*xunsafe.Field - selectors []*structology.Selector - unwrapper *xunsafe.Field + // Promoted Raw.Col{i} fields on OUTER wrapper + fields []*xunsafe.Field + + // structology selectors resolved on OUTER with path "Actual.<...>" (codec targets) + selectors []*structology.Selector + + // OUTER.Shadow (idx=1) and OUTER.Actual (idx=2) + shadowField *xunsafe.Field + unwrapperActual *xunsafe.Field + + // Back-compat alias so view.go references to v._codec.unwrapper still compile + unwrapper *xunsafe.Field + + // OUTER wrapper reflect type actualType reflect.Type - columns []*Column + + // Columns that have codecs + columns []*Column + + // Actual pointer handling + actualIsPtr bool + actualElemTyp reflect.Type + + // Shadow -> Actual copy (safe reflect-based) + shadowFieldNames []string // exported field names mirrored in Shadow and Actual } ) func newColumnsCodec(viewType reflect.Type, columns []*Column) (*columnsCodec, error) { var withCodec []*Column for i, column := range columns { - if column.Codec != nil { + if column != nil && column.Codec != nil { withCodec = append(withCodec, columns[i]) } } - if len(withCodec) == 0 { return nil, nil } - - codec := &columnsCodec{} - - if err := codec.init(viewType, withCodec); err != nil { + cc := &columnsCodec{} + if err := cc.init(viewType, withCodec); err != nil { return nil, err } - - return codec, nil + return cc, nil } func (c *columnsCodec) init(viewType reflect.Type, columns []*Column) error { c.columns = columns + + // Build Raw holder: Col0..ColN with sqlx tags that match result set column names. codecStructFields := make([]reflect.StructField, len(columns)) for i, column := range columns { scanType := columnDatabaseScanType(column) @@ -60,38 +85,100 @@ func (c *columnsCodec) init(viewType reflect.Type, columns []*Column) error { Tag: reflect.StructTag(fmt.Sprintf(`sqlx:"%v"`, column.Name)), } } - rawType := reflect.StructOf(codecStructFields) + + // Determine Actual element type and build Shadow (exported fields only, preserving tags). + c.actualIsPtr = viewType.Kind() == reflect.Ptr + c.actualElemTyp = viewType + if c.actualIsPtr { + c.actualElemTyp = viewType.Elem() + } + shadowFields := make([]reflect.StructField, 0, c.actualElemTyp.NumField()) + c.shadowFieldNames = make([]string, 0, c.actualElemTyp.NumField()) + for i := 0; i < c.actualElemTyp.NumField(); i++ { + f := c.actualElemTyp.Field(i) + // Skip unexported fields + if f.PkgPath != "" { + continue + } + shadowFields = append(shadowFields, reflect.StructField{ + Name: f.Name, + Type: f.Type, + Tag: f.Tag, // preserve sqlx tags for scanning + }) + c.shadowFieldNames = append(c.shadowFieldNames, f.Name) + } + shadowType := reflect.StructOf(shadowFields) + + // OUTER wrapper layout: + // - Raw (embedded, first) + // - Shadow (embedded, second) + // - Actual (named, third) c.actualType = reflect.StructOf([]reflect.StructField{ { Name: rawFieldName, Type: rawType, Anonymous: true, }, + { + Name: shadowFieldName, + Type: shadowType, + Anonymous: true, + }, { Name: actualFieldName, Type: viewType, - Anonymous: true, + Anonymous: false, // non-embedded to avoid reflect panic if model has methods }, }) + // Promoted Raw.Col{i} on OUTER c.fields = make([]*xunsafe.Field, len(columns)) for i := 0; i < len(columns); i++ { - c.fields[i] = xunsafe.FieldByIndex(rawType, i) + colName := "Col" + strconv.Itoa(i) + c.fields[i] = xunsafe.FieldByName(c.actualType, colName) } - c.unwrapper = xunsafe.FieldByIndex(c.actualType, 1) - stateType := structology.NewStateType(c.actualType, structology.WithCustomizedNames(func(name string, tag reflect.StructTag) []string { - sqlxTag := io.ParseTag(tag) - if sqlxTag.Column == "" { - return []string{name} - } - return strings.Split(sqlxTag.Column, "|") - })) + // Shadow and Actual fields on OUTER + // Indexes: 0=Raw, 1=Shadow, 2=Actual + c.shadowField = xunsafe.FieldByIndex(c.actualType, 1) + c.unwrapperActual = xunsafe.FieldByIndex(c.actualType, 2) + // Back-compat alias so view.go can still use v._codec.unwrapper + c.unwrapper = c.unwrapperActual + // Build structology state for OUTER wrapper (honor sqlx tags anywhere) + stateType := structology.NewStateType( + c.actualType, + structology.WithCustomizedNames(func(name string, tag reflect.StructTag) []string { + sqlxTag := io.ParseTag(tag) + if sqlxTag.Column == "" { + return []string{name} + } + return strings.Split(sqlxTag.Column, "|") + }), + ) + + // Build selectors on OUTER using "Actual." (codec targets) for _, column := range columns { - c.selectors = append(c.selectors, stateType.Lookup(actualFieldName+"."+column.Name)) + var sel *structology.Selector + candidates := []string{ + column.Name, // exact alias + strings.ToLower(column.Name), // lowercase alias + toUpperCamel(column.Name), // Go-style name + } + for _, cand := range candidates { + if cand == "" { + continue + } + path := actualFieldName + "." + cand + sel = stateType.Lookup(path) + if sel != nil { + break + } + } + c.selectors = append(c.selectors, sel) } + return nil } @@ -106,17 +193,113 @@ func columnDatabaseScanType(column *Column) reflect.Type { } func (c *columnsCodec) updateValue(ctx context.Context, value interface{}, record *codec2.ParentValue) error { - asPtr := xunsafe.AsPointer(value) + // OUTER wrapper pointer (used by xunsafe/selectors) + outerPtr := xunsafe.AsPointer(value) + + // 1) Ensure OUTER.Actual is non-nil if Actual is a pointer type + if c.actualIsPtr { + curr := c.unwrapperActual.Value(outerPtr) // interface{} of *Elem or nil + needsAlloc := false + if curr == nil { + needsAlloc = true + } else { + rv := reflect.ValueOf(curr) + if rv.Kind() == reflect.Ptr && rv.IsNil() { + needsAlloc = true + } + } + if needsAlloc { + if c.actualElemTyp == nil { + return fmt.Errorf("invalid Actual element type") + } + newVal := reflect.New(c.actualElemTyp).Interface() // *Elem + c.unwrapperActual.SetValue(outerPtr, newVal) + } + } + + // 2) SAFE Shadow -> Actual copy via reflect (avoid unsafe header corruption) + // Build a live reflect.Value view over OUTER + outerRV := reflect.NewAt(c.actualType, outerPtr).Elem() + + // shadowRV is the embedded Shadow struct value + shadowRV := outerRV.FieldByName(shadowFieldName) + + // actualRV is the Actual field (struct or pointer-to-struct) + actualRV := outerRV.FieldByName(actualFieldName) + var actualElemRV reflect.Value + if c.actualIsPtr { + // ensure non-nil (already ensured above) + if actualRV.IsNil() { + actualRV.Set(reflect.New(c.actualElemTyp)) + } + actualElemRV = actualRV.Elem() + } else { + actualElemRV = actualRV + } + + // Copy exported fields by name + for _, name := range c.shadowFieldNames { + dst := actualElemRV.FieldByName(name) + if !dst.IsValid() || !dst.CanSet() { + continue + } + src := shadowRV.FieldByName(name) + if !src.IsValid() { + continue + } + if src.Type().AssignableTo(dst.Type()) { + dst.Set(src) + continue + } + if src.Type().ConvertibleTo(dst.Type()) { + dst.Set(src.Convert(dst.Type())) + continue + } + // Otherwise, skip incompatible types (codec may overwrite later) + } + + // 3) Apply codecs: read raw DB value from promoted Col{i} on OUTER, decode, and set via selectors for i, column := range c.columns { - fieldValue := c.fields[i].Value(asPtr) - decoded, err := column.Codec.Transform(ctx, fieldValue, codec.WithOptions(record)) + if c.fields[i] == nil { + return fmt.Errorf("codec raw field not found for column %q", column.Name) + } + if c.selectors[i] == nil { + return fmt.Errorf("codec selector not found for column %q (tried Actual.)", column.Name) + } + raw := c.fields[i].Value(outerPtr) + decoded, err := column.Codec.Transform(ctx, raw, codec.WithOptions(record)) if err != nil { return err } - if err = c.selectors[i].SetValue(asPtr, decoded); err != nil { + // Selector root is OUTER (path "Actual.<...>") + if err = c.selectors[i].SetValue(outerPtr, decoded); err != nil { return err } } - return nil } + +// toUpperCamel converts snake/space/hyphen/dot separated names to UpperCamel. +// "AD_ORDERS_DATA_INDEX" -> "AdOrdersDataIndex" +func toUpperCamel(s string) string { + if s == "" { + return s + } + var b strings.Builder + b.Grow(len(s)) + capNext := true + for _, r := range s { + switch r { + case '_', '-', ' ', '.': + capNext = true + continue + } + if capNext { + b.WriteRune(unicode.ToUpper(r)) + capNext = false + } else { + b.WriteRune(unicode.ToLower(r)) + } + } + return b.String() +}