Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,31 @@ func collectFKBindings(node *graph.Node) []FKBinding {
}

// assignFKs sets FK fields on the node based on its parent edges.
//
// All bindings for the node share a single allocation: we materialize one
// addressable pointer to the value, copy every parent PK into the matching
// FK field, then store the resulting struct back on the node. The copy goes
// through [field.Copy], which uses cached field-index paths and avoids boxing
// the PK through `any` once per binding.
func assignFKs(node *graph.Node) error {
for _, edge := range node.Dependencies() {
parent := edge.Parent
deps := node.Dependencies()
if len(deps) == 0 {
return nil
}

ptr := reflect.New(reflect.TypeOf(node.Value))
ptr.Elem().Set(reflect.ValueOf(node.Value))
ptr := reflect.New(reflect.TypeOf(node.Value))
ptr.Elem().Set(reflect.ValueOf(node.Value))
target := ptr.Interface()

for _, edge := range deps {
parent := edge.Parent
for _, binding := range edge.Bindings {
pkVal, err := field.GetField(parent.Value, binding.ParentField)
if err != nil {
return fmt.Errorf("get parent field %q for node %q: %w", binding.ParentField, parent.ID, err)
}
if err := field.SetField(ptr.Interface(), binding.ChildField, pkVal); err != nil {
return fmt.Errorf("set child field %q for node %q: %w", binding.ChildField, node.ID, err)
if err := field.Copy(parent.Value, binding.ParentField, target, binding.ChildField); err != nil {
return fmt.Errorf("bind parent %q field %q to node %q field %q: %w",
parent.ID, binding.ParentField, node.ID, binding.ChildField, err)
}
}
node.Value = ptr.Elem().Interface()
}
node.Value = ptr.Elem().Interface()
return nil
}
9 changes: 5 additions & 4 deletions internal/field/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ func GetField(v any, name string) (any, error) {
return nil, fmt.Errorf("%w: GetField requires a struct or pointer to struct", errx.ErrInvalidOption)
}

field := rv.FieldByName(name)
if !field.IsValid() {
return nil, fmt.Errorf("get field %q: %w", name, errx.FieldNotFoundWithHint(rv.Type().Name(), name, exportedFields(rv.Type())))
rt := rv.Type()
entry, ok := lookupFieldIndex(rt, name)
if !ok {
return nil, fmt.Errorf("get field %q: %w", name, errx.FieldNotFoundWithHint(rt.Name(), name, exportedFields(rt)))
}

return field.Interface(), nil
return rv.FieldByIndex(entry.Index).Interface(), nil
}
92 changes: 89 additions & 3 deletions internal/field/lookup.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
package field

import (
"fmt"
"reflect"
"sort"
"sync"

"github.com/mhiro2/seedling/internal/errx"
)

var (
exportedFieldCache sync.Map
fieldIndexCache sync.Map
)

var exportedFieldCache sync.Map
// fieldIndexKey identifies a (struct type, field name) pair.
type fieldIndexKey struct {
Type reflect.Type
Name string
}

// fieldIndexEntry caches the resolved field index path and metadata for a
// given struct type / field name pair. Only successful lookups are stored to
// keep the cache bounded under adversarial inputs (e.g. fuzz tests).
type fieldIndexEntry struct {
Index []int
Type reflect.Type
Exported bool
}

// lookupFieldIndex resolves a field index path for the given type and name,
// caching the result. The cache stores only successful lookups so that
// pathological miss patterns (random field names) cannot grow it unbounded.
func lookupFieldIndex(rt reflect.Type, name string) (fieldIndexEntry, bool) {
key := fieldIndexKey{Type: rt, Name: name}
if v, ok := fieldIndexCache.Load(key); ok {
return v.(fieldIndexEntry), true
}
f, ok := rt.FieldByName(name)
if !ok {
return fieldIndexEntry{}, false
}
entry := fieldIndexEntry{
Index: append([]int(nil), f.Index...),
Type: f.Type,
Exported: f.IsExported(),
}
fieldIndexCache.Store(key, entry)
return entry, true
}

// Exists reports whether the struct type has an exported field with the given name.
func Exists(v any, name string) bool {
Expand All @@ -20,8 +62,52 @@ func Exists(v any, name string) bool {
if rt.Kind() != reflect.Struct {
return false
}
f, ok := rt.FieldByName(name)
return ok && f.IsExported()
entry, ok := lookupFieldIndex(rt, name)
return ok && entry.Exported
}

// Copy reads srcName from src (struct or *struct) and assigns the value to
// dstName on dstPtr (must be *struct). It avoids the boxing round-trip that
// using GetField + SetField would incur, and reuses the cached field index
// from lookupFieldIndex so the hot path performs no FieldByName lookups after
// the first call for a given (type, name) pair.
func Copy(src any, srcName string, dstPtr any, dstName string) error {
srcVal := reflect.ValueOf(src)
if srcVal.Kind() == reflect.Pointer {
srcVal = srcVal.Elem()
}
if srcVal.Kind() != reflect.Struct {
return fmt.Errorf("%w: source must be a struct or pointer to struct", errx.ErrInvalidOption)
}
srcType := srcVal.Type()
srcEntry, ok := lookupFieldIndex(srcType, srcName)
if !ok {
return fmt.Errorf("get field %q: %w", srcName, errx.FieldNotFoundWithHint(srcType.Name(), srcName, exportedFields(srcType)))
}

dstRV := reflect.ValueOf(dstPtr)
if dstRV.Kind() != reflect.Pointer || dstRV.Elem().Kind() != reflect.Struct {
return fmt.Errorf("%w: destination must be a pointer to struct", errx.ErrInvalidOption)
}
dstElem := dstRV.Elem()
dstType := dstElem.Type()
dstEntry, ok := lookupFieldIndex(dstType, dstName)
if !ok {
return fmt.Errorf("set field %q: %w", dstName, errx.FieldNotFoundWithHint(dstType.Name(), dstName, exportedFields(dstType)))
}

srcField := srcVal.FieldByIndex(srcEntry.Index)
dstField := dstElem.FieldByIndex(dstEntry.Index)

if !dstField.CanSet() {
return fmt.Errorf("%w: field %q is unexported", errx.ErrFieldNotFound, dstName)
}
if !srcField.Type().AssignableTo(dstField.Type()) {
return fmt.Errorf("set field %q: %w", dstName, errx.TypeMismatch(dstName, dstField.Type().String(), srcField.Type().String()))
}

dstField.Set(srcField)
return nil
}

// exportedFields returns the sorted names of all exported fields on a struct type.
Expand Down
10 changes: 7 additions & 3 deletions internal/field/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ func SetField(ptr any, name string, value any) error {
return fmt.Errorf("%w: SetField requires a pointer to struct", errx.ErrInvalidOption)
}

field := rv.Elem().FieldByName(name)
if !field.IsValid() {
return fmt.Errorf("set field %q: %w", name, errx.FieldNotFoundWithHint(rv.Elem().Type().Name(), name, exportedFields(rv.Elem().Type())))
elem := rv.Elem()
rt := elem.Type()
entry, ok := lookupFieldIndex(rt, name)
if !ok {
return fmt.Errorf("set field %q: %w", name, errx.FieldNotFoundWithHint(rt.Name(), name, exportedFields(rt)))
}

field := elem.FieldByIndex(entry.Index)
if !field.CanSet() {
return fmt.Errorf("%w: field %q is unexported", errx.ErrFieldNotFound, name)
}
Expand Down
Loading