Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ resources:
grants:
- principal: deco-test-user@databricks.com
privileges:
- CREATE_TABLE
- USE_SCHEMA
- CREATE_TABLE
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"changes": [
{
"add": [
"CREATE_TABLE",
"USE_SCHEMA"
"USE_SCHEMA",
"CREATE_TABLE"
],
"principal": "deco-test-user@databricks.com",
"remove": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
{
"principal": "deco-test-user@databricks.com",
"privileges": [
"CREATE_TABLE",
"USE_SCHEMA"
"USE_SCHEMA",
"CREATE_TABLE"
]
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@
},
"changes": {
"local": {
"grants[0].privileges[0]": {
"grants[principal='deco-test-user@databricks.com'].privileges[='APPLY_TAG']": {
"action": "update"
},
"grants[0].privileges[1]": {
"grants[principal='deco-test-user@databricks.com'].privileges[='USE_SCHEMA']": {
"action": "update"
}
}
Expand Down
4 changes: 2 additions & 2 deletions bundle/direct/bundle_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
// for integers: compare 0 with actual object ID. As long as real object IDs are never 0 we're good.
// Once we add non-id fields or add per-field details to "bundle plan", we must read dynamic data and deal with references as first class citizen.
// This means distinguishing between 0 that are actually object ids and 0 that are there because typed struct integer cannot contain ${...} string.
localDiff, err := structdiff.GetStructDiff(savedState, entry.NewState.Value, adapter.KeyedSlices())
localDiff, err := structdiff.GetStructDiff(savedState, entry.NewState.Value, adapter.KeyedSliceTrie())
if err != nil {
logdiag.LogError(ctx, fmt.Errorf("%s: diffing local state: %w", errorPrefix, err))
return false
Expand Down Expand Up @@ -187,7 +187,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
return false
}

remoteDiff, err := structdiff.GetStructDiff(savedState, remoteStateComparable, adapter.KeyedSlices())
remoteDiff, err := structdiff.GetStructDiff(savedState, remoteStateComparable, adapter.KeyedSliceTrie())
if err != nil {
logdiag.LogError(ctx, fmt.Errorf("%s: diffing remote state: %w", errorPrefix, err))
return false
Expand Down
18 changes: 17 additions & 1 deletion bundle/direct/dresources/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"github.com/databricks/cli/bundle/deployplan"
"github.com/databricks/cli/libs/calladapt"
"github.com/databricks/cli/libs/structs/structdiff"
"github.com/databricks/cli/libs/structs/structtrie"
"github.com/databricks/databricks-sdk-go"
)

Expand Down Expand Up @@ -106,7 +107,8 @@

fieldTriggersLocal map[string]deployplan.ActionType
fieldTriggersRemote map[string]deployplan.ActionType
keyedSlices map[string]any
// keyedSlices map[string]any
keyedSliceTrie *structtrie.Node
}

func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, error) {
Expand Down Expand Up @@ -136,7 +138,7 @@
classifyChange: nil,
fieldTriggersLocal: map[string]deployplan.ActionType{},
fieldTriggersRemote: map[string]deployplan.ActionType{},
keyedSlices: nil,

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / validate-generated-is-up-to-date

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, terraform)

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, direct)

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, terraform)

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, direct)

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / lint

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, terraform)

unknown field keyedSlices in struct literal of type Adapter

Check failure on line 141 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, direct)

unknown field keyedSlices in struct literal of type Adapter
}

err = adapter.initMethods(impl)
Expand Down Expand Up @@ -273,10 +275,20 @@
return err
}
if keyedSlicesCall != nil {
a.keyedSlices, err = loadKeyedSlices(keyedSlicesCall)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / validate-generated-is-up-to-date

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / lint

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 278 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)
if err != nil {
return err
}
if len(a.keyedSlices) > 0 {

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / validate-generated-is-up-to-date

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / lint

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 282 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)
typed := make(map[string]structdiff.KeyFunc, len(a.keyedSlices))

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / validate-generated-is-up-to-date

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / lint

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 283 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)
for pattern, fn := range a.keyedSlices {

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / validate-generated-is-up-to-date

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / lint

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 284 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)
typed[pattern] = fn
}
a.keyedSliceTrie, err = structdiff.BuildSliceKeyTrie(typed)
if err != nil {
return err
}
}
}

return nil
Expand Down Expand Up @@ -612,9 +624,13 @@
// KeyedSlices returns a map from path patterns to KeyFunc for comparing slices by key.
// If the resource doesn't implement KeyedSlices, returns nil.
func (a *Adapter) KeyedSlices() map[string]any {
return a.keyedSlices

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / validate-generated-is-up-to-date

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, linux-ubuntu-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (macos-latest, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / lint

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices) (typecheck)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, terraform)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)

Check failure on line 627 in bundle/direct/dresources/adapter.go

View workflow job for this annotation

GitHub Actions / tests (databricks-protected-runner-group-large, windows-server-latest-large, direct)

a.keyedSlices undefined (type *Adapter has no field or method keyedSlices, but does have method KeyedSlices)
}

func (a *Adapter) KeyedSliceTrie() *structtrie.Node {
return a.keyedSliceTrie
}

// prepareCallRequired prepares a call and ensures the method is found.
func prepareCallRequired(resource any, methodName string) (*calladapt.BoundCaller, error) {
caller, err := calladapt.PrepareCall(resource, calladapt.TypeOf[IResource](), methodName)
Expand Down
25 changes: 15 additions & 10 deletions bundle/direct/dresources/grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"reflect"
"sort"
"strings"

"github.com/databricks/cli/libs/structs/structvar"
Expand Down Expand Up @@ -85,9 +84,6 @@ func PrepareGrantsInputConfig(inputConfig any, node string) (*structvar.StructVa
privileges = append(privileges, catalog.Privilege(item.String()))
}

// Backend sorts privileges, so we sort here as well.
sortPriviliges(privileges)

grants = append(grants, GrantAssignment{
Principal: principal,
Privileges: privileges,
Expand Down Expand Up @@ -118,6 +114,21 @@ func (*ResourceGrants) PrepareState(state *GrantsState) *GrantsState {
return state
}

func privilegeKey(x catalog.Privilege) (string, string, error) {
return "", string(x), nil
}

func grantAssignmentKey(x GrantAssignment) (string, string, error) {
return "principal", x.Principal, nil
}

func (*ResourceGrants) KeyedSlices(s *GrantsState) map[string]any {
return map[string]any{
"grants": grantAssignmentKey,
"grants[*].privileges": privilegeKey,
}
}

func (r *ResourceGrants) DoRead(ctx context.Context, id string) (*GrantsState, error) {
securableType, fullName, err := parseGrantsID(id)
if err != nil {
Expand Down Expand Up @@ -214,12 +225,6 @@ func (r *ResourceGrants) listGrants(ctx context.Context, securableType, fullName
return assignments, nil
}

func sortPriviliges(privileges []catalog.Privilege) {
sort.Slice(privileges, func(i, j int) bool {
return privileges[i] < privileges[j]
})
}

func extractGrantResourceType(node string) (string, error) {
rest, ok := strings.CutPrefix(node, "resources.")
if !ok {
Expand Down
124 changes: 53 additions & 71 deletions libs/structs/structdiff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"reflect"
"slices"
"sort"
"strings"

"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structtag"
"github.com/databricks/cli/libs/structs/structtrie"
)

type Change struct {
Expand Down Expand Up @@ -58,23 +58,45 @@ func (c *keyFuncCaller) call(elem any) (string, string) {
return keyField, keyValue
}

// diffContext holds configuration for the diff operation.
type diffContext struct {
sliceKeys map[string]KeyFunc
func keyFuncFor(node *structtrie.Node) KeyFunc {
if node == nil {
return nil
}
if value := node.Value(); value != nil {
if fn, ok := value.(KeyFunc); ok {
return fn
}
}
return nil
}

// BuildSliceKeyTrie converts a map of slice-key patterns to a PrefixTree used by GetStructDiff.
// Returns nil if sliceKeys is empty.
func BuildSliceKeyTrie(sliceKeys map[string]KeyFunc) (*structtrie.Node, error) {
if len(sliceKeys) == 0 {
return nil, nil
}

root := structtrie.New()
for pattern, fn := range sliceKeys {
_, err := newKeyFuncCaller(fn)
if err != nil {
return nil, err
}
if _, err := structtrie.InsertString(root, pattern, fn); err != nil {
return nil, err
}
}
return root, nil
}

// GetStructDiff compares two Go structs and returns a list of Changes or an error.
// Respects ForceSendFields if present.
// Types of a and b must match exactly, otherwise returns an error.
//
// The sliceKeys parameter maps path patterns to functions that extract
// key field/value pairs from slice elements. When provided, slices at matching
// paths are compared as maps keyed by (keyField, keyValue) instead of by index.
// Path patterns use dot notation (e.g., "tasks" or "job.tasks").
// The [*] wildcard matches any slice index in the path.
// Note, key wildcard is not supported yet ("a.*.c")
// Pass nil if no slice key functions are needed.
func GetStructDiff(a, b any, sliceKeys map[string]KeyFunc) ([]Change, error) {
// The sliceTrie parameter is produced by BuildSliceKeyTrie and allows comparing slices
// as maps keyed by (keyField, keyValue). Pass nil if no keyed slices are needed.
func GetStructDiff(a, b any, sliceTrie *structtrie.Node) ([]Change, error) {
v1 := reflect.ValueOf(a)
v2 := reflect.ValueOf(b)

Expand All @@ -93,16 +115,15 @@ func GetStructDiff(a, b any, sliceKeys map[string]KeyFunc) ([]Change, error) {
return nil, fmt.Errorf("type mismatch: %v vs %v", v1.Type(), v2.Type())
}

ctx := &diffContext{sliceKeys: sliceKeys}
if err := diffValues(ctx, nil, v1, v2, &changes); err != nil {
if err := diffValues(sliceTrie, nil, v1, v2, &changes); err != nil {
return nil, err
}
return changes, nil
}

// diffValues appends changes between v1 and v2 to the slice. path is the current
// JSON-style path (dot + brackets). At the root path is "".
func diffValues(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.Value, changes *[]Change) error {
func diffValues(trieNode *structtrie.Node, path *structpath.PathNode, v1, v2 reflect.Value, changes *[]Change) error {
if !v1.IsValid() {
if !v2.IsValid() {
return nil
Expand Down Expand Up @@ -145,25 +166,26 @@ func diffValues(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.Valu

switch kind {
case reflect.Pointer:
return diffValues(ctx, path, v1.Elem(), v2.Elem(), changes)
return diffValues(trieNode, path, v1.Elem(), v2.Elem(), changes)
case reflect.Struct:
return diffStruct(ctx, path, v1, v2, changes)
return diffStruct(trieNode, path, v1, v2, changes)
case reflect.Slice, reflect.Array:
if keyFunc := ctx.findKeyFunc(path); keyFunc != nil {
return diffSliceByKey(ctx, path, v1, v2, keyFunc, changes)
if keyFunc := keyFuncFor(trieNode); keyFunc != nil {
return diffSliceByKey(trieNode, path, v1, v2, keyFunc, changes)
} else if v1.Len() != v2.Len() {
*changes = append(*changes, Change{Path: path, Old: v1.Interface(), New: v2.Interface()})
} else {
for i := range v1.Len() {
node := structpath.NewIndex(path, i)
if err := diffValues(ctx, node, v1.Index(i), v2.Index(i), changes); err != nil {
nextTrie := trieNode.Child(node)
if err := diffValues(nextTrie, node, v1.Index(i), v2.Index(i), changes); err != nil {
return err
}
}
}
case reflect.Map:
if v1Type.Key().Kind() == reflect.String {
return diffMapStringKey(ctx, path, v1, v2, changes)
return diffMapStringKey(trieNode, path, v1, v2, changes)
} else {
deepEqualValues(path, v1, v2, changes)
}
Expand All @@ -179,7 +201,7 @@ func deepEqualValues(path *structpath.PathNode, v1, v2 reflect.Value, changes *[
}
}

func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Change) error {
func diffStruct(trieNode *structtrie.Node, path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Change) error {
t := s1.Type()
forced1 := getForceSendFields(s1)
forced2 := getForceSendFields(s2)
Expand All @@ -192,7 +214,7 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu

// Continue traversing embedded structs. Do not add the key to the path though.
if sf.Anonymous {
if err := diffValues(ctx, path, s1.Field(i), s2.Field(i), changes); err != nil {
if err := diffValues(trieNode, path, s1.Field(i), s2.Field(i), changes); err != nil {
return err
}
continue
Expand Down Expand Up @@ -228,14 +250,15 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu
}
}

if err := diffValues(ctx, node, v1Field, v2Field, changes); err != nil {
nextTrie := trieNode.Child(node)
if err := diffValues(nextTrie, node, v1Field, v2Field, changes); err != nil {
return err
}
}
return nil
}

func diffMapStringKey(ctx *diffContext, path *structpath.PathNode, m1, m2 reflect.Value, changes *[]Change) error {
func diffMapStringKey(trieNode *structtrie.Node, path *structpath.PathNode, m1, m2 reflect.Value, changes *[]Change) error {
keySet := map[string]reflect.Value{}
for _, k := range m1.MapKeys() {
// Key is always string at this point
Expand All @@ -258,7 +281,8 @@ func diffMapStringKey(ctx *diffContext, path *structpath.PathNode, m1, m2 reflec
v1 := m1.MapIndex(k)
v2 := m2.MapIndex(k)
node := structpath.NewStringKey(path, ks)
if err := diffValues(ctx, node, v1, v2, changes); err != nil {
nextTrie := trieNode.Child(node)
if err := diffValues(nextTrie, node, v1, v2, changes); err != nil {
return err
}
}
Expand All @@ -280,49 +304,6 @@ func getForceSendFields(v reflect.Value) []string {
return nil
}

// findKeyFunc returns the KeyFunc for the given path, or nil if none matches.
// Path patterns support [*] to match any slice index.
func (ctx *diffContext) findKeyFunc(path *structpath.PathNode) KeyFunc {
if ctx.sliceKeys == nil {
return nil
}
pathStr := pathToPattern(path)
return ctx.sliceKeys[pathStr]
}

// pathToPattern converts a PathNode to a pattern string for matching.
// Slice indices are converted to [*] wildcard.
func pathToPattern(path *structpath.PathNode) string {
if path == nil {
return ""
}

components := path.AsSlice()
var result strings.Builder

for i, node := range components {
if idx, ok := node.Index(); ok {
// Convert numeric index to wildcard
_ = idx
result.WriteString("[*]")
} else if key, value, ok := node.KeyValue(); ok {
// Key-value syntax
result.WriteString("[")
result.WriteString(key)
result.WriteString("=")
result.WriteString(structpath.EncodeMapKey(value))
result.WriteString("]")
} else if key, ok := node.StringKey(); ok {
if i != 0 {
result.WriteString(".")
}
result.WriteString(key)
}
}

return result.String()
}

// sliceElement holds a slice element with its key information.
type sliceElement struct {
keyField string
Expand All @@ -346,7 +327,7 @@ func validateKeyFuncElementType(seq reflect.Value, expected reflect.Type) error
// diffSliceByKey compares two slices using the provided key function.
// Elements are matched by their (keyField, keyValue) pairs instead of by index.
// Duplicate keys are allowed and matched in order.
func diffSliceByKey(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.Value, keyFunc KeyFunc, changes *[]Change) error {
func diffSliceByKey(trieNode *structtrie.Node, path *structpath.PathNode, v1, v2 reflect.Value, keyFunc KeyFunc, changes *[]Change) error {
caller, err := newKeyFuncCaller(keyFunc)
if err != nil {
return err
Expand Down Expand Up @@ -404,7 +385,8 @@ func diffSliceByKey(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.
minLen := min(len(list1), len(list2))
for i := range minLen {
node := structpath.NewKeyValue(path, keyField, keyValue)
if err := diffValues(ctx, node, list1[i].value, list2[i].value, changes); err != nil {
nextTrie := trieNode.Child(node)
if err := diffValues(nextTrie, node, list1[i].value, list2[i].value, changes); err != nil {
return err
}
}
Expand Down
Loading
Loading