Skip to content

Commit 899c941

Browse files
committed
wip
1 parent 031fec4 commit 899c941

File tree

6 files changed

+162
-158
lines changed

6 files changed

+162
-158
lines changed

bundle/direct/bundle_plan.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
144144
// for integers: compare 0 with actual object ID. As long as real object IDs are never 0 we're good.
145145
// 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.
146146
// This means distinguishing between 0 that are actually object ids and 0 that are there because typed struct integer cannot contain ${...} string.
147-
localDiff, err := structdiff.GetStructDiff(savedState, entry.NewState.Value, adapter.KeyedSlices())
147+
localDiff, err := structdiff.GetStructDiff(savedState, entry.NewState.Value, adapter.KeyedSliceTrie())
148148
if err != nil {
149149
logdiag.LogError(ctx, fmt.Errorf("%s: diffing local state: %w", errorPrefix, err))
150150
return false
@@ -187,7 +187,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks
187187
return false
188188
}
189189

190-
remoteDiff, err := structdiff.GetStructDiff(savedState, remoteStateComparable, adapter.KeyedSlices())
190+
remoteDiff, err := structdiff.GetStructDiff(savedState, remoteStateComparable, adapter.KeyedSliceTrie())
191191
if err != nil {
192192
logdiag.LogError(ctx, fmt.Errorf("%s: diffing remote state: %w", errorPrefix, err))
193193
return false

bundle/direct/dresources/adapter.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/databricks/cli/bundle/deployplan"
1111
"github.com/databricks/cli/libs/calladapt"
1212
"github.com/databricks/cli/libs/structs/structdiff"
13+
"github.com/databricks/cli/libs/structs/structtrie"
1314
"github.com/databricks/databricks-sdk-go"
1415
)
1516

@@ -107,6 +108,7 @@ type Adapter struct {
107108
fieldTriggersLocal map[string]deployplan.ActionType
108109
fieldTriggersRemote map[string]deployplan.ActionType
109110
keyedSlices map[string]any
111+
keyedSliceTrie *structtrie.Node
110112
}
111113

112114
func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, error) {
@@ -277,6 +279,16 @@ func (a *Adapter) initMethods(resource any) error {
277279
if err != nil {
278280
return err
279281
}
282+
if len(a.keyedSlices) > 0 {
283+
typed := make(map[string]structdiff.KeyFunc, len(a.keyedSlices))
284+
for pattern, fn := range a.keyedSlices {
285+
typed[pattern] = fn
286+
}
287+
a.keyedSliceTrie, err = structdiff.BuildSliceKeyTrie(typed)
288+
if err != nil {
289+
return err
290+
}
291+
}
280292
}
281293

282294
return nil
@@ -615,6 +627,10 @@ func (a *Adapter) KeyedSlices() map[string]any {
615627
return a.keyedSlices
616628
}
617629

630+
func (a *Adapter) KeyedSliceTrie() *structtrie.Node {
631+
return a.keyedSliceTrie
632+
}
633+
618634
// prepareCallRequired prepares a call and ensures the method is found.
619635
func prepareCallRequired(resource any, methodName string) (*calladapt.BoundCaller, error) {
620636
caller, err := calladapt.PrepareCall(resource, calladapt.TypeOf[IResource](), methodName)

libs/structs/structdiff/diff.go

Lines changed: 61 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import (
55
"reflect"
66
"slices"
77
"sort"
8-
"strings"
98

109
"github.com/databricks/cli/libs/structs/structpath"
1110
"github.com/databricks/cli/libs/structs/structtag"
11+
"github.com/databricks/cli/libs/structs/structtrie"
1212
)
1313

1414
type Change struct {
@@ -58,23 +58,53 @@ func (c *keyFuncCaller) call(elem any) (string, string) {
5858
return keyField, keyValue
5959
}
6060

61-
// diffContext holds configuration for the diff operation.
62-
type diffContext struct {
63-
sliceKeys map[string]KeyFunc
61+
func advanceTrie(root *structtrie.Node, node *structtrie.Node, pathNode *structpath.PathNode) *structtrie.Node {
62+
if root == nil {
63+
return nil
64+
}
65+
if node == nil {
66+
node = root
67+
}
68+
return node.Child(pathNode)
69+
}
70+
71+
func keyFuncFor(node *structtrie.Node) KeyFunc {
72+
if node == nil {
73+
return nil
74+
}
75+
if value := node.Value(); value != nil {
76+
return value.(*keyFuncCaller)
77+
}
78+
return nil
79+
}
80+
81+
// BuildSliceKeyTrie converts a map of slice-key patterns to a PrefixTree used by GetStructDiff.
82+
// Returns nil if sliceKeys is empty.
83+
func BuildSliceKeyTrie(sliceKeys map[string]KeyFunc) (*structtrie.Node, error) {
84+
if len(sliceKeys) == 0 {
85+
return nil, nil
86+
}
87+
88+
root := structtrie.New()
89+
for pattern, fn := range sliceKeys {
90+
caller, err := newKeyFuncCaller(fn)
91+
if err != nil {
92+
return nil, err
93+
}
94+
if _, err := structtrie.InsertString(root, pattern, caller); err != nil {
95+
return nil, err
96+
}
97+
}
98+
return root, nil
6499
}
65100

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

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

96-
ctx := &diffContext{sliceKeys: sliceKeys}
97-
if err := diffValues(ctx, nil, v1, v2, &changes); err != nil {
126+
if err := diffValues(sliceTrie, sliceTrie, nil, v1, v2, &changes); err != nil {
98127
return nil, err
99128
}
100129
return changes, nil
101130
}
102131

103132
// diffValues appends changes between v1 and v2 to the slice. path is the current
104133
// JSON-style path (dot + brackets). At the root path is "".
105-
func diffValues(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.Value, changes *[]Change) error {
134+
func diffValues(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, v1, v2 reflect.Value, changes *[]Change) error {
106135
if !v1.IsValid() {
107136
if !v2.IsValid() {
108137
return nil
@@ -145,25 +174,26 @@ func diffValues(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.Valu
145174

146175
switch kind {
147176
case reflect.Pointer:
148-
return diffValues(ctx, path, v1.Elem(), v2.Elem(), changes)
177+
return diffValues(trieRoot, trieNode, path, v1.Elem(), v2.Elem(), changes)
149178
case reflect.Struct:
150-
return diffStruct(ctx, path, v1, v2, changes)
179+
return diffStruct(trieRoot, trieNode, path, v1, v2, changes)
151180
case reflect.Slice, reflect.Array:
152-
if keyFunc := ctx.findKeyFunc(path); keyFunc != nil {
153-
return diffSliceByKey(ctx, path, v1, v2, keyFunc, changes)
181+
if keyFunc := keyFuncFor(trieNode); keyFunc != nil {
182+
return diffSliceByKey(trieRoot, trieNode, path, v1, v2, keyFunc, changes)
154183
} else if v1.Len() != v2.Len() {
155184
*changes = append(*changes, Change{Path: path, Old: v1.Interface(), New: v2.Interface()})
156185
} else {
157186
for i := range v1.Len() {
158187
node := structpath.NewIndex(path, i)
159-
if err := diffValues(ctx, node, v1.Index(i), v2.Index(i), changes); err != nil {
188+
nextTrie := advanceTrie(trieRoot, trieNode, node)
189+
if err := diffValues(trieRoot, nextTrie, node, v1.Index(i), v2.Index(i), changes); err != nil {
160190
return err
161191
}
162192
}
163193
}
164194
case reflect.Map:
165195
if v1Type.Key().Kind() == reflect.String {
166-
return diffMapStringKey(ctx, path, v1, v2, changes)
196+
return diffMapStringKey(trieRoot, trieNode, path, v1, v2, changes)
167197
} else {
168198
deepEqualValues(path, v1, v2, changes)
169199
}
@@ -179,7 +209,7 @@ func deepEqualValues(path *structpath.PathNode, v1, v2 reflect.Value, changes *[
179209
}
180210
}
181211

182-
func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Change) error {
212+
func diffStruct(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Change) error {
183213
t := s1.Type()
184214
forced1 := getForceSendFields(s1)
185215
forced2 := getForceSendFields(s2)
@@ -192,7 +222,7 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu
192222

193223
// Continue traversing embedded structs. Do not add the key to the path though.
194224
if sf.Anonymous {
195-
if err := diffValues(ctx, path, s1.Field(i), s2.Field(i), changes); err != nil {
225+
if err := diffValues(trieRoot, trieNode, path, s1.Field(i), s2.Field(i), changes); err != nil {
196226
return err
197227
}
198228
continue
@@ -228,14 +258,15 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu
228258
}
229259
}
230260

231-
if err := diffValues(ctx, node, v1Field, v2Field, changes); err != nil {
261+
nextTrie := advanceTrie(trieRoot, trieNode, node)
262+
if err := diffValues(trieRoot, nextTrie, node, v1Field, v2Field, changes); err != nil {
232263
return err
233264
}
234265
}
235266
return nil
236267
}
237268

238-
func diffMapStringKey(ctx *diffContext, path *structpath.PathNode, m1, m2 reflect.Value, changes *[]Change) error {
269+
func diffMapStringKey(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, m1, m2 reflect.Value, changes *[]Change) error {
239270
keySet := map[string]reflect.Value{}
240271
for _, k := range m1.MapKeys() {
241272
// Key is always string at this point
@@ -258,7 +289,8 @@ func diffMapStringKey(ctx *diffContext, path *structpath.PathNode, m1, m2 reflec
258289
v1 := m1.MapIndex(k)
259290
v2 := m2.MapIndex(k)
260291
node := structpath.NewStringKey(path, ks)
261-
if err := diffValues(ctx, node, v1, v2, changes); err != nil {
292+
nextTrie := advanceTrie(trieRoot, trieNode, node)
293+
if err := diffValues(trieRoot, nextTrie, node, v1, v2, changes); err != nil {
262294
return err
263295
}
264296
}
@@ -280,50 +312,6 @@ func getForceSendFields(v reflect.Value) []string {
280312
return nil
281313
}
282314

283-
// findKeyFunc returns the KeyFunc for the given path, or nil if none matches.
284-
// Path patterns support [*] to match any slice index.
285-
func (ctx *diffContext) findKeyFunc(path *structpath.PathNode) KeyFunc {
286-
if ctx.sliceKeys == nil {
287-
return nil
288-
}
289-
pathStr := pathToPattern(path)
290-
fmt.Printf("looking up %q\n", pathStr)
291-
return ctx.sliceKeys[pathStr]
292-
}
293-
294-
// pathToPattern converts a PathNode to a pattern string for matching.
295-
// Slice indices are converted to [*] wildcard.
296-
func pathToPattern(path *structpath.PathNode) string {
297-
if path == nil {
298-
return ""
299-
}
300-
301-
components := path.AsSlice()
302-
var result strings.Builder
303-
304-
for i, node := range components {
305-
if idx, ok := node.Index(); ok {
306-
// Convert numeric index to wildcard
307-
_ = idx
308-
result.WriteString("[*]")
309-
} else if key, value, ok := node.KeyValue(); ok {
310-
// Key-value syntax
311-
result.WriteString("[")
312-
result.WriteString(key)
313-
result.WriteString("=")
314-
result.WriteString(structpath.EncodeMapKey(value))
315-
result.WriteString("]")
316-
} else if key, ok := node.StringKey(); ok {
317-
if i != 0 {
318-
result.WriteString(".")
319-
}
320-
result.WriteString(key)
321-
}
322-
}
323-
324-
return result.String()
325-
}
326-
327315
// sliceElement holds a slice element with its key information.
328316
type sliceElement struct {
329317
keyField string
@@ -347,7 +335,7 @@ func validateKeyFuncElementType(seq reflect.Value, expected reflect.Type) error
347335
// diffSliceByKey compares two slices using the provided key function.
348336
// Elements are matched by their (keyField, keyValue) pairs instead of by index.
349337
// Duplicate keys are allowed and matched in order.
350-
func diffSliceByKey(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.Value, keyFunc KeyFunc, changes *[]Change) error {
338+
func diffSliceByKey(trieRoot *structtrie.Node, trieNode *structtrie.Node, path *structpath.PathNode, v1, v2 reflect.Value, keyFunc KeyFunc, changes *[]Change) error {
351339
caller, err := newKeyFuncCaller(keyFunc)
352340
if err != nil {
353341
return err
@@ -405,7 +393,8 @@ func diffSliceByKey(ctx *diffContext, path *structpath.PathNode, v1, v2 reflect.
405393
minLen := min(len(list1), len(list2))
406394
for i := range minLen {
407395
node := structpath.NewKeyValue(path, keyField, keyValue)
408-
if err := diffValues(ctx, node, list1[i].value, list2[i].value, changes); err != nil {
396+
nextTrie := advanceTrie(trieRoot, trieNode, node)
397+
if err := diffValues(trieRoot, nextTrie, node, list1[i].value, list2[i].value, changes); err != nil {
409398
return err
410399
}
411400
}

0 commit comments

Comments
 (0)