From 35c3a859eb1a28c1dde2de84e85c26711b77d8c8 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 27 Nov 2025 14:37:16 +0100 Subject: [PATCH 1/8] wip --- bundle/direct/dresources/grants.go | 25 +++++++++++++++---------- libs/structs/structdiff/diff.go | 1 + 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index 121aaf6084..44bbd71d4d 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "reflect" - "sort" "strings" "github.com/databricks/cli/libs/structs/structvar" @@ -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, @@ -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 { @@ -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 { diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index 160f14b649..f52e1451a9 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -287,6 +287,7 @@ func (ctx *diffContext) findKeyFunc(path *structpath.PathNode) KeyFunc { return nil } pathStr := pathToPattern(path) + fmt.Printf("looking up %q\n", pathStr) return ctx.sliceKeys[pathStr] } From 81273a183bd2529b365d7c7a1c4f48f1e393df6e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 27 Nov 2025 15:37:12 +0100 Subject: [PATCH 2/8] add prefixtree --- libs/structs/structprefixtree/prefixtree.go | 369 ++++++++++++++++++ .../structprefixtree/prefixtree_test.go | 228 +++++++++++ 2 files changed, 597 insertions(+) create mode 100644 libs/structs/structprefixtree/prefixtree.go create mode 100644 libs/structs/structprefixtree/prefixtree_test.go diff --git a/libs/structs/structprefixtree/prefixtree.go b/libs/structs/structprefixtree/prefixtree.go new file mode 100644 index 0000000000..b037c84e2e --- /dev/null +++ b/libs/structs/structprefixtree/prefixtree.go @@ -0,0 +1,369 @@ +package structprefixtree + +import ( + "fmt" + + "github.com/databricks/cli/libs/structs/structpath" +) + +/* + +Prefix tree stores a map of path pattern to values. + +Both concrete paths and patterns can be matched against the tree to find the matching value. + +In case of multiple matches, longest and most concrete match wins. + +Tree can be construct from map[string]any but internal format is more efficient. + +Input: + +{"*": "star", + "grants": "grants slice", + "grants[*]": "grant", + "grants[*].principal": "principal", +} + +Would match: + "some_path" => "star", + "grants" => "grants slice", + "some_path.no_key" => nil (no match), + "grants[0].principal" => "principal", + "grants[1].principal" => "principal", + "grants[*].principal" => "principal", + +Prefix tree also supports efficient iteration, going to child and parent nodes. + +Parent(): return parent node or nil for root +Child(node *PathNode) return child node in the tree best match matching node. Note, node.Parent() is not used here, only PathNode itself. + + +*/ + +// PrefixTree stores path patterns and associated values in a trie-like structure. +// Matches prefer the deepest node, with concrete segments taking precedence over wildcards. +type PrefixTree struct { + Root *Node +} + +// Node represents a single step inside the prefix tree. +// It keeps track of the component that leads to it, its value and the children below it. +type Node struct { + parent *Node + component componentKey + children map[componentKey]*Node + value any +} + +// NewPrefixTree returns an empty prefix tree with a root node. +func NewPrefixTree() *PrefixTree { + return &PrefixTree{Root: newNode(componentKey{}, nil)} +} + +// NewPrefixTreeFromMap constructs a prefix tree from serialized path patterns. +func NewPrefixTreeFromMap(values map[string]any) (*PrefixTree, error) { + tree := NewPrefixTree() + for raw, v := range values { + path, err := structpath.Parse(raw) + if err != nil { + return nil, fmt.Errorf("parse %q: %w", raw, err) + } + if _, err = tree.Insert(path, v); err != nil { + return nil, fmt.Errorf("insert %q: %w", raw, err) + } + } + return tree, nil +} + +// Insert adds or updates a value for the given path pattern. +// A nil path represents the root node. +func (t *PrefixTree) Insert(path *structpath.PathNode, value any) (*Node, error) { + if t.Root == nil { + t.Root = newNode(componentKey{}, nil) + } + + if path == nil { + if t.Root.value != nil { + return nil, fmt.Errorf("path %q already exists", "") + } + t.Root.value = value + return t.Root, nil + } + + current := t.Root + for _, segment := range path.AsSlice() { + key, err := componentFromPattern(segment) + if err != nil { + return nil, err + } + if current.children == nil { + current.children = make(map[componentKey]*Node) + } + child, exists := current.children[key] + if !exists { + child = newNode(key, current) + current.children[key] = child + } + current = child + } + + if current.value != nil { + return nil, fmt.Errorf("path %q already exists", path.String()) + } + + current.value = value + return current, nil +} + +// InsertString parses the string path pattern and inserts the value. +func (t *PrefixTree) InsertString(path string, value any) (*Node, error) { + parsed, err := structpath.Parse(path) + if err != nil { + return nil, err + } + return t.Insert(parsed, value) +} + +// Match returns the node with the best matching value for the provided path. +// Matches prefer the deepest node. When depth ties, the node that used fewer wildcards wins. +func (t *PrefixTree) Match(path *structpath.PathNode) (*Node, bool) { + if t == nil || t.Root == nil { + return nil, false + } + + if path == nil { + if t.Root.value != nil { + return t.Root, true + } + return nil, false + } + + segments := path.AsSlice() + var best matchResult + t.match(t.Root, segments, 0, 0, 0, &best) + + if best.node != nil { + return best.node, true + } + + return nil, false +} + +// MatchString parses the given path string and matches it against the tree. +func (t *PrefixTree) MatchString(path string) (*Node, bool, error) { + parsed, err := structpath.Parse(path) + if err != nil { + return nil, false, err + } + node, ok := t.Match(parsed) + return node, ok, nil +} + +// Parent returns the parent node or nil for the root. +func (n *Node) Parent() *Node { + if n == nil { + return nil + } + return n.parent +} + +// Child returns the child node that best matches the provided PathNode. +// Exact matches are preferred. When unavailable, wildcard children are returned. +func (n *Node) Child(pathNode *structpath.PathNode) *Node { + if n == nil || pathNode == nil { + return nil + } + + if pathNode.DotStar() || pathNode.BracketStar() { + return n.childFor(wildcardComponent) + } + + if key, ok := pathNode.StringKey(); ok { + if child := n.childFor(componentKey{kind: componentKindExact, key: key}); child != nil { + return child + } + } + + return n.childFor(wildcardComponent) +} + +// Children returns all child nodes. +func (n *Node) Children() []*Node { + if n == nil || len(n.children) == 0 { + return nil + } + + result := make([]*Node, 0, len(n.children)) + for _, child := range n.children { + result = append(result, child) + } + return result +} + +// Value returns the stored value. +func (n *Node) Value() any { + if n == nil { + return nil + } + return n.value +} + +/* +// SetValue overwrites the stored value. +func (n *Node) SetValue(value any) { + if n == nil { + return + } + n.value = value +} + +// HasValue indicates whether the node stores a non-nil value. +func (n *Node) HasValue() bool { + if n == nil { + return false + } + return n.value != nil +}*/ +/* +// Path returns the full path from the root to this node. +func (n *Node) Path() *structpath.PathNode { + if n == nil || n.parent == nil { + return nil + } + return n.component.append(n.parent.Path()) +}*/ + +func (n *Node) childFor(key componentKey) *Node { + if n == nil || len(n.children) == 0 { + return nil + } + return n.children[key] +} + +func newNode(component componentKey, parent *Node) *Node { + return &Node{ + parent: parent, + component: component, + } +} + +type componentKind uint8 + +const ( + componentKindInvalid componentKind = iota + componentKindExact + componentKindWildcard +) + +type componentKey struct { + kind componentKind + key string +} + +var wildcardComponent = componentKey{kind: componentKindWildcard} + +func componentFromPattern(node *structpath.PathNode) (componentKey, error) { + if node == nil { + return componentKey{}, fmt.Errorf("nil path node") + } + + if node.DotStar() || node.BracketStar() { + return wildcardComponent, nil + } + + if _, ok := node.Index(); ok { + return componentKey{}, fmt.Errorf("array indexes are not supported in prefix tree keys") + } + + if _, _, ok := node.KeyValue(); ok { + return componentKey{}, fmt.Errorf("key-value selectors are not supported in prefix tree keys") + } + + if key, ok := node.StringKey(); ok { + return componentKey{ + kind: componentKindExact, + key: key, + }, nil + } + + return componentKey{}, fmt.Errorf("unsupported prefix tree component %q", node.String()) +} + +/* +func (c componentKey) append(prev *structpath.PathNode) *structpath.PathNode { + switch c.kind { + case componentKindExact: + return structpath.NewStringKey(prev, c.key) + case componentKindWildcard: + return structpath.NewDotStar(prev) + default: + return prev + } +}*/ + +func (t *PrefixTree) match(current *Node, segments []*structpath.PathNode, index int, depth int, concreteness int, best *matchResult) { + if current == nil { + return + } + + if index == len(segments) { + best.consider(current, depth, concreteness) + return + } + + children := current.matchingChildren(segments[index]) + for _, child := range children { + nextConcreteness := concreteness + if child.component.kind != componentKindWildcard { + nextConcreteness++ + } + t.match(child, segments, index+1, depth+1, nextConcreteness, best) + } +} + +func (n *Node) matchingChildren(pathNode *structpath.PathNode) []*Node { + if len(n.children) == 0 { + return nil + } + + if pathNode == nil { + return nil + } + + if pathNode.DotStar() || pathNode.BracketStar() { + if child, exists := n.children[wildcardComponent]; exists { + return []*Node{child} + } + return nil + } + + var out []*Node + if key, ok := pathNode.StringKey(); ok { + if child, exists := n.children[componentKey{kind: componentKindExact, key: key}]; exists { + out = append(out, child) + } + } + if child, exists := n.children[wildcardComponent]; exists { + out = append(out, child) + } + return out +} + +type matchResult struct { + node *Node + depth int + concreteness int +} + +func (m *matchResult) consider(node *Node, depth int, concreteness int) { + if node == nil || node.value == nil { + return + } + if m.node == nil || + depth > m.depth || + (depth == m.depth && concreteness > m.concreteness) { + m.node = node + m.depth = depth + m.concreteness = concreteness + } +} diff --git a/libs/structs/structprefixtree/prefixtree_test.go b/libs/structs/structprefixtree/prefixtree_test.go new file mode 100644 index 0000000000..755ac2d630 --- /dev/null +++ b/libs/structs/structprefixtree/prefixtree_test.go @@ -0,0 +1,228 @@ +package structprefixtree + +import ( + "testing" + + "github.com/databricks/cli/libs/structs/structpath" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPrefixTreeMatchesSample(t *testing.T) { + tree, err := NewPrefixTreeFromMap(map[string]any{ + "*": "star", + "grants": "grants slice", + "grants[*]": "grant", + "grants[*].principal": "principal", + "foo": "foo", + "foo[*]": "foo slice", + "foo[*].id": "foo slice id", + "foo.bar.id": "foo bar id", + }) + require.NoError(t, err) + + tests := []struct { + name string + path string + value any + }{ + { + name: "fallback wildcard", + path: "some_path", + value: "star", + }, + { + name: "direct match", + path: "grants", + value: "grants slice", + }, + { + name: "wildcard element", + path: "grants[0]", + value: "grant", + }, + { + name: "deep wildcard element", + path: "grants[1].principal", + value: "principal", + }, + { + name: "pattern input", + path: "grants[*].principal", + value: "principal", + }, + { + name: "deepest concrete key", + path: "foo.bar.id", + value: "foo bar id", + }, + { + name: "fallback to wildcard at same depth", + path: "foo[4].id", + value: "foo slice id", + }, + { + name: "fallback to element wildcard", + path: "foo[3]", + value: "foo slice", + }, + { + name: "fallback to root wildcard", + path: "bar", + value: "star", + }, + { + name: "no match", + path: "some_path.no_key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node, ok, err := tree.MatchString(tt.path) + require.NoError(t, err) + wantMatch := tt.value != nil + assert.Equal(t, wantMatch, ok) + if wantMatch { + require.NotNil(t, node) + assert.Equal(t, tt.value, node.Value()) + } else { + assert.Nil(t, node) + } + }) + } +} + +func TestChildPrefersExactThenWildcard(t *testing.T) { + tree := NewPrefixTree() + mustInsert(t, tree, "*", "star") + mustInsert(t, tree, "foo", "foo") + + root := tree.Root + require.NotNil(t, root) + + exact := root.Child(mustParse(t, "foo")) + require.NotNil(t, exact) + assert.Equal(t, "foo", exact.Value()) + assert.Equal(t, root, exact.Parent()) + + wildcard := root.Child(mustParse(t, "bar")) + require.NotNil(t, wildcard) + assert.Equal(t, "star", wildcard.Value()) + assert.Equal(t, root, wildcard.Parent()) +} + +func TestDotStarAndBracketStarAreEquivalent(t *testing.T) { + tree := NewPrefixTree() + mustInsert(t, tree, "items[*].name", "value") + + mustMatch(t, tree, "items.*.name", "value") + mustMatch(t, tree, "items[5].name", "value") +} + +func TestRootValueMatch(t *testing.T) { + tree := NewPrefixTree() + _, err := tree.Insert(nil, "root") + require.NoError(t, err) + + node, ok := tree.Match(nil) + require.True(t, ok) + assert.Equal(t, "root", node.Value()) + + node, ok, err = tree.MatchString("any") + require.NoError(t, err) + assert.False(t, ok) + assert.Nil(t, node) +} + +func TestWildcardMatchesKeyValueAndIndex(t *testing.T) { + tree := NewPrefixTree() + mustInsert(t, tree, "items.*.name", "value") + + mustMatch(t, tree, "items[task_key='foo'].name", "value") + mustMatch(t, tree, "items[3].name", "value") +} + +func TestPatternMustConsumeEntirePath(t *testing.T) { + tree := NewPrefixTree() + mustInsert(t, tree, "*", "star") + + node, ok, err := tree.MatchString("foo.bar") + require.NoError(t, err) + assert.False(t, ok) + assert.Nil(t, node) +} + +func TestInsertRejectsIndexAndKeyValue(t *testing.T) { + tree := NewPrefixTree() + + _, err := tree.InsertString("foo[1]", "x") + require.Error(t, err) + + _, err = tree.InsertString("foo[key='value']", "x") + require.Error(t, err) +} + +func TestNewPrefixTreeFromMapRejectsPathWithIndex(t *testing.T) { + _, err := NewPrefixTreeFromMap(map[string]any{ + "foo": "ok", + "foo[1]": "bad", + }) + require.EqualError(t, err, `insert "foo[1]": array indexes are not supported in prefix tree keys`) +} + +func TestNewPrefixTreeFromMapRejectsDuplicateWildcardPaths(t *testing.T) { + _, err := NewPrefixTreeFromMap(map[string]any{ + "items.*": "value-1", + "items[*]": "value-2", + }) + require.ErrorContains(t, err, `already exists`) +} + +func TestInsertRejectsDuplicatePaths(t *testing.T) { + tree := NewPrefixTree() + path, err := structpath.Parse("foo.bar") + require.NoError(t, err) + + _, err = tree.Insert(path, "value-1") + require.NoError(t, err) + + _, err = tree.Insert(path, "value-2") + require.Error(t, err) +} + +func TestInsertStringRejectsDuplicatePaths(t *testing.T) { + tree := NewPrefixTree() + + _, err := tree.InsertString("foo.bar", "value-1") + require.NoError(t, err) + + _, err = tree.InsertString("foo.bar", "value-2") + require.Error(t, err) +} + +func mustParse(t *testing.T, path string) *structpath.PathNode { + t.Helper() + if path == "" { + return nil + } + p, err := structpath.Parse(path) + require.NoError(t, err) + return p +} + +func mustInsert(t *testing.T, tree *PrefixTree, path string, value any) { + t.Helper() + _, err := tree.InsertString(path, value) + require.NoError(t, err) +} + +func mustMatch(t *testing.T, tree *PrefixTree, path string, expected any) *Node { + t.Helper() + node, ok, err := tree.MatchString(path) + require.NoError(t, err) + require.True(t, ok, "expected match for %s", path) + require.NotNil(t, node) + assert.Equal(t, expected, node.Value()) + return node +} From 031fec484e7241e2742dacfd6f61a7829cb4cde7 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 27 Nov 2025 15:39:00 +0100 Subject: [PATCH 3/8] rename to trie --- .../{structprefixtree/prefixtree.go => structtrie/trie.go} | 6 +++--- .../prefixtree_test.go => structtrie/trie_test.go} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename libs/structs/{structprefixtree/prefixtree.go => structtrie/trie.go} (98%) rename libs/structs/{structprefixtree/prefixtree_test.go => structtrie/trie_test.go} (99%) diff --git a/libs/structs/structprefixtree/prefixtree.go b/libs/structs/structtrie/trie.go similarity index 98% rename from libs/structs/structprefixtree/prefixtree.go rename to libs/structs/structtrie/trie.go index b037c84e2e..32f5e37eac 100644 --- a/libs/structs/structprefixtree/prefixtree.go +++ b/libs/structs/structtrie/trie.go @@ -1,4 +1,4 @@ -package structprefixtree +package structtrie import ( "fmt" @@ -301,7 +301,7 @@ func (c componentKey) append(prev *structpath.PathNode) *structpath.PathNode { } }*/ -func (t *PrefixTree) match(current *Node, segments []*structpath.PathNode, index int, depth int, concreteness int, best *matchResult) { +func (t *PrefixTree) match(current *Node, segments []*structpath.PathNode, index, depth, concreteness int, best *matchResult) { if current == nil { return } @@ -355,7 +355,7 @@ type matchResult struct { concreteness int } -func (m *matchResult) consider(node *Node, depth int, concreteness int) { +func (m *matchResult) consider(node *Node, depth, concreteness int) { if node == nil || node.value == nil { return } diff --git a/libs/structs/structprefixtree/prefixtree_test.go b/libs/structs/structtrie/trie_test.go similarity index 99% rename from libs/structs/structprefixtree/prefixtree_test.go rename to libs/structs/structtrie/trie_test.go index 755ac2d630..fb074aaaea 100644 --- a/libs/structs/structprefixtree/prefixtree_test.go +++ b/libs/structs/structtrie/trie_test.go @@ -1,4 +1,4 @@ -package structprefixtree +package structtrie import ( "testing" From 899c941226234407c835e5539431a0a35f70f54a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 27 Nov 2025 17:05:41 +0100 Subject: [PATCH 4/8] wip --- bundle/direct/bundle_plan.go | 4 +- bundle/direct/dresources/adapter.go | 16 ++++ libs/structs/structdiff/diff.go | 133 ++++++++++++--------------- libs/structs/structdiff/diff_test.go | 34 ++++--- libs/structs/structtrie/trie.go | 60 ++++++------ libs/structs/structtrie/trie_test.go | 73 +++++++-------- 6 files changed, 162 insertions(+), 158 deletions(-) diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 7d8ebf9c51..3bb3bb955b 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -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 @@ -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 diff --git a/bundle/direct/dresources/adapter.go b/bundle/direct/dresources/adapter.go index 71279b8706..cf83637cab 100644 --- a/bundle/direct/dresources/adapter.go +++ b/bundle/direct/dresources/adapter.go @@ -10,6 +10,7 @@ import ( "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" ) @@ -107,6 +108,7 @@ type Adapter struct { fieldTriggersLocal map[string]deployplan.ActionType fieldTriggersRemote map[string]deployplan.ActionType keyedSlices map[string]any + keyedSliceTrie *structtrie.Node } func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, error) { @@ -277,6 +279,16 @@ func (a *Adapter) initMethods(resource any) error { if err != nil { return err } + if len(a.keyedSlices) > 0 { + typed := make(map[string]structdiff.KeyFunc, len(a.keyedSlices)) + for pattern, fn := range a.keyedSlices { + typed[pattern] = fn + } + a.keyedSliceTrie, err = structdiff.BuildSliceKeyTrie(typed) + if err != nil { + return err + } + } } return nil @@ -615,6 +627,10 @@ func (a *Adapter) KeyedSlices() map[string]any { return a.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) diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index f52e1451a9..01097d3218 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -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 { @@ -58,23 +58,53 @@ 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 advanceTrie(root *structtrie.Node, node *structtrie.Node, pathNode *structpath.PathNode) *structtrie.Node { + if root == nil { + return nil + } + if node == nil { + node = root + } + return node.Child(pathNode) +} + +func keyFuncFor(node *structtrie.Node) KeyFunc { + if node == nil { + return nil + } + if value := node.Value(); value != nil { + return value.(*keyFuncCaller) + } + 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 { + caller, err := newKeyFuncCaller(fn) + if err != nil { + return nil, err + } + if _, err := structtrie.InsertString(root, pattern, caller); 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) @@ -93,8 +123,7 @@ 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, sliceTrie, nil, v1, v2, &changes); err != nil { return nil, err } return changes, nil @@ -102,7 +131,7 @@ func GetStructDiff(a, b any, sliceKeys map[string]KeyFunc) ([]Change, error) { // 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(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, v1, v2 reflect.Value, changes *[]Change) error { if !v1.IsValid() { if !v2.IsValid() { return nil @@ -145,25 +174,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(trieRoot, trieNode, path, v1.Elem(), v2.Elem(), changes) case reflect.Struct: - return diffStruct(ctx, path, v1, v2, changes) + return diffStruct(trieRoot, 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(trieRoot, 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 := advanceTrie(trieRoot, trieNode, node) + if err := diffValues(trieRoot, 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(trieRoot, trieNode, path, v1, v2, changes) } else { deepEqualValues(path, v1, v2, changes) } @@ -179,7 +209,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(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Change) error { t := s1.Type() forced1 := getForceSendFields(s1) forced2 := getForceSendFields(s2) @@ -192,7 +222,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(trieRoot, trieNode, path, s1.Field(i), s2.Field(i), changes); err != nil { return err } continue @@ -228,14 +258,15 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu } } - if err := diffValues(ctx, node, v1Field, v2Field, changes); err != nil { + nextTrie := advanceTrie(trieRoot, trieNode, node) + if err := diffValues(trieRoot, 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(trieRoot *structtrie.Node, 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 @@ -258,7 +289,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 := advanceTrie(trieRoot, trieNode, node) + if err := diffValues(trieRoot, nextTrie, node, v1, v2, changes); err != nil { return err } } @@ -280,50 +312,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) - fmt.Printf("looking up %q\n", pathStr) - 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 @@ -347,7 +335,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(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, v1, v2 reflect.Value, keyFunc KeyFunc, changes *[]Change) error { caller, err := newKeyFuncCaller(keyFunc) if err != nil { return err @@ -405,7 +393,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 := advanceTrie(trieRoot, trieNode, node) + if err := diffValues(trieRoot, nextTrie, node, list1[i].value, list2[i].value, changes); err != nil { return err } } diff --git a/libs/structs/structdiff/diff_test.go b/libs/structs/structdiff/diff_test.go index 9a216e0c4f..df57cdc451 100644 --- a/libs/structs/structdiff/diff_test.go +++ b/libs/structs/structdiff/diff_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/databricks/cli/libs/structs/structtrie" "github.com/stretchr/testify/assert" ) @@ -64,6 +65,15 @@ func resolveChanges(changes []Change) []ResolvedChange { return resolved } +func mustSliceTrie(t *testing.T, sliceKeys map[string]KeyFunc) *structtrie.Node { + t.Helper() + trie, err := BuildSliceKeyTrie(sliceKeys) + if err != nil { + t.Fatalf("failed to build slice trie: %v", err) + } + return trie +} + func TestGetStructDiff(t *testing.T) { b1 := &B{S: "one"} b2 := &B{S: "two"} @@ -456,6 +466,7 @@ func TestGetStructDiffSliceKeys(t *testing.T) { sliceKeys := map[string]KeyFunc{ "tasks": taskKeyFunc, } + sliceTrie := mustSliceTrie(t, sliceKeys) tests := []struct { name string @@ -514,7 +525,7 @@ func TestGetStructDiffSliceKeys(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetStructDiff(tt.a, tt.b, sliceKeys) + got, err := GetStructDiff(tt.a, tt.b, sliceTrie) assert.NoError(t, err) assert.Equal(t, tt.want, resolveChanges(got)) }) @@ -542,6 +553,7 @@ func TestGetStructDiffNestedSliceKeys(t *testing.T) { sliceKeys := map[string]KeyFunc{ "nested[*].items": itemKeyFunc, } + sliceTrie := mustSliceTrie(t, sliceKeys) tests := []struct { name string @@ -570,7 +582,7 @@ func TestGetStructDiffNestedSliceKeys(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetStructDiff(tt.a, tt.b, sliceKeys) + got, err := GetStructDiff(tt.a, tt.b, sliceTrie) assert.NoError(t, err) assert.Equal(t, tt.want, resolveChanges(got)) }) @@ -617,10 +629,7 @@ func TestGetStructDiffSliceKeysInvalidFunc(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sliceKeys := map[string]KeyFunc{"tasks": tt.keyFunc} - a := Job{Tasks: []Task{{TaskKey: "a"}}} - b := Job{Tasks: []Task{{TaskKey: "a"}}} - _, err := GetStructDiff(a, b, sliceKeys) + _, err := BuildSliceKeyTrie(map[string]KeyFunc{"tasks": tt.keyFunc}) assert.EqualError(t, err, tt.errMsg) }) } @@ -628,21 +637,20 @@ func TestGetStructDiffSliceKeysInvalidFunc(t *testing.T) { func TestGetStructDiffSliceKeysWrongArgType(t *testing.T) { // Function expects Item but slice contains Task - sliceKeys := map[string]KeyFunc{ + sliceTrie, err := BuildSliceKeyTrie(map[string]KeyFunc{ "tasks": func(item Item) (string, string) { return "id", item.ID }, - } + }) + assert.NoError(t, err) a := Job{Tasks: []Task{{TaskKey: "a"}}} b := Job{Tasks: []Task{{TaskKey: "b"}}} - _, err := GetStructDiff(a, b, sliceKeys) + _, err = GetStructDiff(a, b, sliceTrie) assert.EqualError(t, err, "KeyFunc expects structdiff.Item, got structdiff.Task") } func TestGetStructDiffSliceKeysDuplicates(t *testing.T) { - sliceKeys := map[string]KeyFunc{ - "tasks": taskKeyFunc, - } + sliceTrie := mustSliceTrie(t, map[string]KeyFunc{"tasks": taskKeyFunc}) tests := []struct { name string @@ -686,7 +694,7 @@ func TestGetStructDiffSliceKeysDuplicates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetStructDiff(tt.a, tt.b, sliceKeys) + got, err := GetStructDiff(tt.a, tt.b, sliceTrie) assert.NoError(t, err) assert.Equal(t, tt.want, resolveChanges(got)) }) diff --git a/libs/structs/structtrie/trie.go b/libs/structs/structtrie/trie.go index 32f5e37eac..26fe8b7978 100644 --- a/libs/structs/structtrie/trie.go +++ b/libs/structs/structtrie/trie.go @@ -40,12 +40,6 @@ Child(node *PathNode) return child node in the tree best match matching node. No */ -// PrefixTree stores path patterns and associated values in a trie-like structure. -// Matches prefer the deepest node, with concrete segments taking precedence over wildcards. -type PrefixTree struct { - Root *Node -} - // Node represents a single step inside the prefix tree. // It keeps track of the component that leads to it, its value and the children below it. type Node struct { @@ -55,42 +49,42 @@ type Node struct { value any } -// NewPrefixTree returns an empty prefix tree with a root node. -func NewPrefixTree() *PrefixTree { - return &PrefixTree{Root: newNode(componentKey{}, nil)} +// New returns an empty prefix tree root node. +func New() *Node { + return newNode(componentKey{}, nil) } -// NewPrefixTreeFromMap constructs a prefix tree from serialized path patterns. -func NewPrefixTreeFromMap(values map[string]any) (*PrefixTree, error) { - tree := NewPrefixTree() +// NewFromMap constructs a prefix tree root from serialized path patterns. +func NewFromMap(values map[string]any) (*Node, error) { + root := New() for raw, v := range values { path, err := structpath.Parse(raw) if err != nil { return nil, fmt.Errorf("parse %q: %w", raw, err) } - if _, err = tree.Insert(path, v); err != nil { + if _, err = Insert(root, path, v); err != nil { return nil, fmt.Errorf("insert %q: %w", raw, err) } } - return tree, nil + return root, nil } // Insert adds or updates a value for the given path pattern. // A nil path represents the root node. -func (t *PrefixTree) Insert(path *structpath.PathNode, value any) (*Node, error) { - if t.Root == nil { - t.Root = newNode(componentKey{}, nil) +func Insert(root *Node, path *structpath.PathNode, value any) (*Node, error) { + if root == nil { + return nil, fmt.Errorf("root cannot be nil") } if path == nil { - if t.Root.value != nil { + if root.value != nil { return nil, fmt.Errorf("path %q already exists", "") } - t.Root.value = value - return t.Root, nil + root.value = value + return root, nil } - current := t.Root + current := root for _, segment := range path.AsSlice() { key, err := componentFromPattern(segment) if err != nil { @@ -116,31 +110,31 @@ func (t *PrefixTree) Insert(path *structpath.PathNode, value any) (*Node, error) } // InsertString parses the string path pattern and inserts the value. -func (t *PrefixTree) InsertString(path string, value any) (*Node, error) { +func InsertString(root *Node, path string, value any) (*Node, error) { parsed, err := structpath.Parse(path) if err != nil { return nil, err } - return t.Insert(parsed, value) + return Insert(root, parsed, value) } // Match returns the node with the best matching value for the provided path. // Matches prefer the deepest node. When depth ties, the node that used fewer wildcards wins. -func (t *PrefixTree) Match(path *structpath.PathNode) (*Node, bool) { - if t == nil || t.Root == nil { +func Match(root *Node, path *structpath.PathNode) (*Node, bool) { + if root == nil { return nil, false } if path == nil { - if t.Root.value != nil { - return t.Root, true + if root.value != nil { + return root, true } return nil, false } segments := path.AsSlice() var best matchResult - t.match(t.Root, segments, 0, 0, 0, &best) + matchFromNode(root, segments, 0, 0, 0, &best) if best.node != nil { return best.node, true @@ -150,12 +144,12 @@ func (t *PrefixTree) Match(path *structpath.PathNode) (*Node, bool) { } // MatchString parses the given path string and matches it against the tree. -func (t *PrefixTree) MatchString(path string) (*Node, bool, error) { +func MatchString(root *Node, path string) (*Node, bool, error) { parsed, err := structpath.Parse(path) if err != nil { return nil, false, err } - node, ok := t.Match(parsed) + node, ok := Match(root, parsed) return node, ok, nil } @@ -301,7 +295,7 @@ func (c componentKey) append(prev *structpath.PathNode) *structpath.PathNode { } }*/ -func (t *PrefixTree) match(current *Node, segments []*structpath.PathNode, index, depth, concreteness int, best *matchResult) { +func matchFromNode(current *Node, segments []*structpath.PathNode, index, depth, concreteness int, best *matchResult) { if current == nil { return } @@ -317,12 +311,12 @@ func (t *PrefixTree) match(current *Node, segments []*structpath.PathNode, index if child.component.kind != componentKindWildcard { nextConcreteness++ } - t.match(child, segments, index+1, depth+1, nextConcreteness, best) + matchFromNode(child, segments, index+1, depth+1, nextConcreteness, best) } } func (n *Node) matchingChildren(pathNode *structpath.PathNode) []*Node { - if len(n.children) == 0 { + if n == nil || len(n.children) == 0 { return nil } diff --git a/libs/structs/structtrie/trie_test.go b/libs/structs/structtrie/trie_test.go index fb074aaaea..3ba1fc156e 100644 --- a/libs/structs/structtrie/trie_test.go +++ b/libs/structs/structtrie/trie_test.go @@ -9,7 +9,7 @@ import ( ) func TestPrefixTreeMatchesSample(t *testing.T) { - tree, err := NewPrefixTreeFromMap(map[string]any{ + root, err := NewFromMap(map[string]any{ "*": "star", "grants": "grants slice", "grants[*]": "grant", @@ -79,7 +79,7 @@ func TestPrefixTreeMatchesSample(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - node, ok, err := tree.MatchString(tt.path) + node, ok, err := MatchString(root, tt.path) require.NoError(t, err) wantMatch := tt.value != nil assert.Equal(t, wantMatch, ok) @@ -94,12 +94,9 @@ func TestPrefixTreeMatchesSample(t *testing.T) { } func TestChildPrefersExactThenWildcard(t *testing.T) { - tree := NewPrefixTree() - mustInsert(t, tree, "*", "star") - mustInsert(t, tree, "foo", "foo") - - root := tree.Root - require.NotNil(t, root) + root := New() + mustInsert(t, root, "*", "star") + mustInsert(t, root, "foo", "foo") exact := root.Child(mustParse(t, "foo")) require.NotNil(t, exact) @@ -113,58 +110,58 @@ func TestChildPrefersExactThenWildcard(t *testing.T) { } func TestDotStarAndBracketStarAreEquivalent(t *testing.T) { - tree := NewPrefixTree() - mustInsert(t, tree, "items[*].name", "value") + root := New() + mustInsert(t, root, "items[*].name", "value") - mustMatch(t, tree, "items.*.name", "value") - mustMatch(t, tree, "items[5].name", "value") + mustMatch(t, root, "items.*.name", "value") + mustMatch(t, root, "items[5].name", "value") } func TestRootValueMatch(t *testing.T) { - tree := NewPrefixTree() - _, err := tree.Insert(nil, "root") + root := New() + _, err := Insert(root, nil, "root") require.NoError(t, err) - node, ok := tree.Match(nil) + node, ok := Match(root, nil) require.True(t, ok) assert.Equal(t, "root", node.Value()) - node, ok, err = tree.MatchString("any") + node, ok, err = MatchString(root, "any") require.NoError(t, err) assert.False(t, ok) assert.Nil(t, node) } func TestWildcardMatchesKeyValueAndIndex(t *testing.T) { - tree := NewPrefixTree() - mustInsert(t, tree, "items.*.name", "value") + root := New() + mustInsert(t, root, "items.*.name", "value") - mustMatch(t, tree, "items[task_key='foo'].name", "value") - mustMatch(t, tree, "items[3].name", "value") + mustMatch(t, root, "items[task_key='foo'].name", "value") + mustMatch(t, root, "items[3].name", "value") } func TestPatternMustConsumeEntirePath(t *testing.T) { - tree := NewPrefixTree() - mustInsert(t, tree, "*", "star") + root := New() + mustInsert(t, root, "*", "star") - node, ok, err := tree.MatchString("foo.bar") + node, ok, err := MatchString(root, "foo.bar") require.NoError(t, err) assert.False(t, ok) assert.Nil(t, node) } func TestInsertRejectsIndexAndKeyValue(t *testing.T) { - tree := NewPrefixTree() + root := New() - _, err := tree.InsertString("foo[1]", "x") + _, err := InsertString(root, "foo[1]", "x") require.Error(t, err) - _, err = tree.InsertString("foo[key='value']", "x") + _, err = InsertString(root, "foo[key='value']", "x") require.Error(t, err) } func TestNewPrefixTreeFromMapRejectsPathWithIndex(t *testing.T) { - _, err := NewPrefixTreeFromMap(map[string]any{ + _, err := NewFromMap(map[string]any{ "foo": "ok", "foo[1]": "bad", }) @@ -172,7 +169,7 @@ func TestNewPrefixTreeFromMapRejectsPathWithIndex(t *testing.T) { } func TestNewPrefixTreeFromMapRejectsDuplicateWildcardPaths(t *testing.T) { - _, err := NewPrefixTreeFromMap(map[string]any{ + _, err := NewFromMap(map[string]any{ "items.*": "value-1", "items[*]": "value-2", }) @@ -180,24 +177,24 @@ func TestNewPrefixTreeFromMapRejectsDuplicateWildcardPaths(t *testing.T) { } func TestInsertRejectsDuplicatePaths(t *testing.T) { - tree := NewPrefixTree() + root := New() path, err := structpath.Parse("foo.bar") require.NoError(t, err) - _, err = tree.Insert(path, "value-1") + _, err = Insert(root, path, "value-1") require.NoError(t, err) - _, err = tree.Insert(path, "value-2") + _, err = Insert(root, path, "value-2") require.Error(t, err) } func TestInsertStringRejectsDuplicatePaths(t *testing.T) { - tree := NewPrefixTree() + root := New() - _, err := tree.InsertString("foo.bar", "value-1") + _, err := InsertString(root, "foo.bar", "value-1") require.NoError(t, err) - _, err = tree.InsertString("foo.bar", "value-2") + _, err = InsertString(root, "foo.bar", "value-2") require.Error(t, err) } @@ -211,15 +208,15 @@ func mustParse(t *testing.T, path string) *structpath.PathNode { return p } -func mustInsert(t *testing.T, tree *PrefixTree, path string, value any) { +func mustInsert(t *testing.T, root *Node, path string, value any) { t.Helper() - _, err := tree.InsertString(path, value) + _, err := InsertString(root, path, value) require.NoError(t, err) } -func mustMatch(t *testing.T, tree *PrefixTree, path string, expected any) *Node { +func mustMatch(t *testing.T, root *Node, path string, expected any) *Node { t.Helper() - node, ok, err := tree.MatchString(path) + node, ok, err := MatchString(root, path) require.NoError(t, err) require.True(t, ok, "expected match for %s", path) require.NotNil(t, node) From a22016e8ee296aaad68b243519be837f4826c968 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 27 Nov 2025 17:11:02 +0100 Subject: [PATCH 5/8] update --- libs/structs/structdiff/diff.go | 46 +++++++++++++-------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index 01097d3218..83b9a524ce 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -58,16 +58,6 @@ func (c *keyFuncCaller) call(elem any) (string, string) { return keyField, keyValue } -func advanceTrie(root *structtrie.Node, node *structtrie.Node, pathNode *structpath.PathNode) *structtrie.Node { - if root == nil { - return nil - } - if node == nil { - node = root - } - return node.Child(pathNode) -} - func keyFuncFor(node *structtrie.Node) KeyFunc { if node == nil { return nil @@ -123,7 +113,7 @@ func GetStructDiff(a, b any, sliceTrie *structtrie.Node) ([]Change, error) { return nil, fmt.Errorf("type mismatch: %v vs %v", v1.Type(), v2.Type()) } - if err := diffValues(sliceTrie, sliceTrie, nil, v1, v2, &changes); err != nil { + if err := diffValues(sliceTrie, nil, v1, v2, &changes); err != nil { return nil, err } return changes, nil @@ -131,7 +121,7 @@ func GetStructDiff(a, b any, sliceTrie *structtrie.Node) ([]Change, error) { // 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(trieRoot *structtrie.Node, trieNode *structtrie.Node, 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 @@ -174,26 +164,26 @@ func diffValues(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *stru switch kind { case reflect.Pointer: - return diffValues(trieRoot, trieNode, path, v1.Elem(), v2.Elem(), changes) + return diffValues(trieNode, path, v1.Elem(), v2.Elem(), changes) case reflect.Struct: - return diffStruct(trieRoot, trieNode, path, v1, v2, changes) + return diffStruct(trieNode, path, v1, v2, changes) case reflect.Slice, reflect.Array: if keyFunc := keyFuncFor(trieNode); keyFunc != nil { - return diffSliceByKey(trieRoot, trieNode, path, v1, v2, keyFunc, changes) + 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) - nextTrie := advanceTrie(trieRoot, trieNode, node) - if err := diffValues(trieRoot, nextTrie, 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(trieRoot, trieNode, path, v1, v2, changes) + return diffMapStringKey(trieNode, path, v1, v2, changes) } else { deepEqualValues(path, v1, v2, changes) } @@ -209,7 +199,7 @@ func deepEqualValues(path *structpath.PathNode, v1, v2 reflect.Value, changes *[ } } -func diffStruct(trieRoot *structtrie.Node, trieNode *structtrie.Node, 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) @@ -222,7 +212,7 @@ func diffStruct(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *stru // Continue traversing embedded structs. Do not add the key to the path though. if sf.Anonymous { - if err := diffValues(trieRoot, trieNode, 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 @@ -258,15 +248,15 @@ func diffStruct(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *stru } } - nextTrie := advanceTrie(trieRoot, trieNode, node) - if err := diffValues(trieRoot, nextTrie, 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(trieRoot *structtrie.Node, trieNode *structtrie.Node, 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 @@ -289,8 +279,8 @@ func diffMapStringKey(trieRoot *structtrie.Node, trieNode *structtrie.Node, path v1 := m1.MapIndex(k) v2 := m2.MapIndex(k) node := structpath.NewStringKey(path, ks) - nextTrie := advanceTrie(trieRoot, trieNode, node) - if err := diffValues(trieRoot, nextTrie, node, v1, v2, changes); err != nil { + nextTrie := trieNode.Child(node) + if err := diffValues(nextTrie, node, v1, v2, changes); err != nil { return err } } @@ -335,7 +325,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(trieRoot *structtrie.Node, trieNode *structtrie.Node, 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 @@ -393,8 +383,8 @@ func diffSliceByKey(trieRoot *structtrie.Node, trieNode *structtrie.Node, path * minLen := min(len(list1), len(list2)) for i := range minLen { node := structpath.NewKeyValue(path, keyField, keyValue) - nextTrie := advanceTrie(trieRoot, trieNode, node) - if err := diffValues(trieRoot, nextTrie, 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 } } From 6accab4b83b0c3f578cbdc2022b781699edc8369 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 27 Nov 2025 17:22:00 +0100 Subject: [PATCH 6/8] update grants test --- .../grants/schemas/change_privilege/databricks.yml.tmpl | 2 +- .../schemas/change_privilege/out.deploy1.requests.direct.json | 4 ++-- .../grants/schemas/change_privilege/out.plan1.direct.json | 4 ++-- .../grants/schemas/change_privilege/out.plan2.direct.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/acceptance/bundle/resources/grants/schemas/change_privilege/databricks.yml.tmpl b/acceptance/bundle/resources/grants/schemas/change_privilege/databricks.yml.tmpl index f73815cfbe..c445b1db01 100644 --- a/acceptance/bundle/resources/grants/schemas/change_privilege/databricks.yml.tmpl +++ b/acceptance/bundle/resources/grants/schemas/change_privilege/databricks.yml.tmpl @@ -9,5 +9,5 @@ resources: grants: - principal: deco-test-user@databricks.com privileges: - - CREATE_TABLE - USE_SCHEMA + - CREATE_TABLE diff --git a/acceptance/bundle/resources/grants/schemas/change_privilege/out.deploy1.requests.direct.json b/acceptance/bundle/resources/grants/schemas/change_privilege/out.deploy1.requests.direct.json index 6c18c8b765..48e65a0063 100644 --- a/acceptance/bundle/resources/grants/schemas/change_privilege/out.deploy1.requests.direct.json +++ b/acceptance/bundle/resources/grants/schemas/change_privilege/out.deploy1.requests.direct.json @@ -5,8 +5,8 @@ "changes": [ { "add": [ - "CREATE_TABLE", - "USE_SCHEMA" + "USE_SCHEMA", + "CREATE_TABLE" ], "principal": "deco-test-user@databricks.com", "remove": [ diff --git a/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan1.direct.json b/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan1.direct.json index b0d9ca1aa7..27dd213866 100644 --- a/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan1.direct.json +++ b/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan1.direct.json @@ -25,8 +25,8 @@ { "principal": "deco-test-user@databricks.com", "privileges": [ - "CREATE_TABLE", - "USE_SCHEMA" + "USE_SCHEMA", + "CREATE_TABLE" ] } ] diff --git a/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan2.direct.json b/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan2.direct.json index 62f205416c..4f2d8ccbd6 100644 --- a/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan2.direct.json +++ b/acceptance/bundle/resources/grants/schemas/change_privilege/out.plan2.direct.json @@ -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" } } From 9873aacf11aa2b438c6c8eeb5db78887753171c1 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 28 Nov 2025 14:34:33 +0100 Subject: [PATCH 7/8] update --- libs/structs/structdiff/diff.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index 83b9a524ce..f811ad0671 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -63,7 +63,9 @@ func keyFuncFor(node *structtrie.Node) KeyFunc { return nil } if value := node.Value(); value != nil { - return value.(*keyFuncCaller) + if fn, ok := value.(KeyFunc); ok { + return fn + } } return nil } @@ -77,11 +79,11 @@ func BuildSliceKeyTrie(sliceKeys map[string]KeyFunc) (*structtrie.Node, error) { root := structtrie.New() for pattern, fn := range sliceKeys { - caller, err := newKeyFuncCaller(fn) + _, err := newKeyFuncCaller(fn) if err != nil { return nil, err } - if _, err := structtrie.InsertString(root, pattern, caller); err != nil { + if _, err := structtrie.InsertString(root, pattern, fn); err != nil { return nil, err } } From c0b1b76c01cb7931259b0aa0307589106fdfd27c Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 28 Nov 2025 16:01:24 +0100 Subject: [PATCH 8/8] wip --- bundle/direct/dresources/adapter.go | 4 ++-- libs/structs/structtrie/trie.go | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bundle/direct/dresources/adapter.go b/bundle/direct/dresources/adapter.go index cf83637cab..23c4f6917a 100644 --- a/bundle/direct/dresources/adapter.go +++ b/bundle/direct/dresources/adapter.go @@ -107,8 +107,8 @@ type Adapter struct { fieldTriggersLocal map[string]deployplan.ActionType fieldTriggersRemote map[string]deployplan.ActionType - keyedSlices map[string]any - keyedSliceTrie *structtrie.Node + // keyedSlices map[string]any + keyedSliceTrie *structtrie.Node } func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, error) { diff --git a/libs/structs/structtrie/trie.go b/libs/structs/structtrie/trie.go index 26fe8b7978..a93c2a028a 100644 --- a/libs/structs/structtrie/trie.go +++ b/libs/structs/structtrie/trie.go @@ -1,6 +1,7 @@ package structtrie import ( + "errors" "fmt" "github.com/databricks/cli/libs/structs/structpath" @@ -73,7 +74,7 @@ func NewFromMap(values map[string]any) (*Node, error) { // A nil path represents the root node. func Insert(root *Node, path *structpath.PathNode, value any) (*Node, error) { if root == nil { - return nil, fmt.Errorf("root cannot be nil") + return nil, errors.New("root cannot be nil") } if path == nil { @@ -258,7 +259,7 @@ var wildcardComponent = componentKey{kind: componentKindWildcard} func componentFromPattern(node *structpath.PathNode) (componentKey, error) { if node == nil { - return componentKey{}, fmt.Errorf("nil path node") + return componentKey{}, errors.New("nil path node") } if node.DotStar() || node.BracketStar() { @@ -266,11 +267,11 @@ func componentFromPattern(node *structpath.PathNode) (componentKey, error) { } if _, ok := node.Index(); ok { - return componentKey{}, fmt.Errorf("array indexes are not supported in prefix tree keys") + return componentKey{}, errors.New("array indexes are not supported in prefix tree keys") } if _, _, ok := node.KeyValue(); ok { - return componentKey{}, fmt.Errorf("key-value selectors are not supported in prefix tree keys") + return componentKey{}, errors.New("key-value selectors are not supported in prefix tree keys") } if key, ok := node.StringKey(); ok {