Skip to content

Commit c9a8fb0

Browse files
direct: wildcard-match UC managed property backend defaults (#5877)
## Summary Use a single wildcard backend-default rule for UC-managed catalog/schema properties: `properties['unity.catalog.managed.*.defaults.*']`. This replaces per-key entries and covers current and future Auto Upgrade defaults without listing each property. ## Why UC fills these keys after create. If we don't treat them as backend defaults, plans keep trying empty updates and UC rejects them. --------- Co-authored-by: Jan N Rose <janniklas.rose@gmail.com>
1 parent f7c9cee commit c9a8fb0

5 files changed

Lines changed: 111 additions & 22 deletions

File tree

.nextchanges/bundles/5877.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* direct: Match UC Auto Upgrade managed property defaults with a wildcard pattern instead of enumerating each key ([#5877](https://github.com/databricks/cli/pull/5877)).

bundle/direct/bundle_plan_test.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,11 @@ func TestShouldSkipBackendDefault_ManagedPropertiesOnly(t *testing.T) {
7777
// Rules mirror the schemas backend_defaults in resources.yml, but the test is
7878
// deliberately self-contained so that edits to resources.yml don't break it.
7979
// The real wiring is covered by acceptance/bundle/resources/schemas/drift.
80-
rowTracking, err := structpath.ParsePattern("properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking']")
81-
require.NoError(t, err)
82-
catalogManaged, err := structpath.ParsePattern("properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged']")
80+
managedDefaults, err := structpath.ParsePattern("properties['unity.catalog.managed.*.defaults.*']")
8381
require.NoError(t, err)
8482
cfg := &dresources.ResourceLifecycleConfig{
8583
BackendDefaults: []dresources.BackendDefaultRule{
86-
{Field: rowTracking},
87-
{Field: catalogManaged},
84+
{Field: managedDefaults},
8885
},
8986
}
9087

@@ -106,12 +103,36 @@ func TestShouldSkipBackendDefault_ManagedPropertiesOnly(t *testing.T) {
106103
remote: "true",
107104
expected: true,
108105
},
106+
{
107+
name: "managed delta cluster by auto property",
108+
path: "properties['unity.catalog.managed.delta.defaults.defaultClusterByAuto']",
109+
remote: "true",
110+
expected: true,
111+
},
112+
{
113+
name: "managed delta checkpoint policy property",
114+
path: "properties['unity.catalog.managed.delta.defaults.delta.checkpointPolicy']",
115+
remote: "v2",
116+
expected: true,
117+
},
118+
{
119+
name: "managed delta feature catalogManaged property",
120+
path: "properties['unity.catalog.managed.delta.defaults.delta.feature.catalogManaged']",
121+
remote: "supported",
122+
expected: true,
123+
},
109124
{
110125
name: "unmanaged remote-only property is not skipped",
111126
path: "properties['custom.remote_only']",
112127
remote: "true",
113128
expected: false,
114129
},
130+
{
131+
name: "managed prefix without defaults segment is not skipped",
132+
path: "properties['unity.catalog.managed.delta.other.delta.enableRowTracking']",
133+
remote: "true",
134+
expected: false,
135+
},
115136
{
116137
name: "managed-only parent properties map is skipped",
117138
path: "properties",

bundle/direct/dresources/resources.yml

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -389,14 +389,8 @@ resources:
389389
- field: name
390390
reason: id_changes
391391
backend_defaults:
392-
# UC auto-populates these system-managed keys after create (same as schemas).
393-
- field: properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking']
394-
- field: properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged']
395-
- field: properties['unity.catalog.managed.delta.defaults.defaultClusterByAuto']
396-
- field: properties['unity.catalog.managed.delta.defaults.delta.checkpointPolicy']
397-
- field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version']
398-
- field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version.afe.internal']
399-
- field: properties['unity.catalog.managed.delta.defaults.delta.feature.catalogManaged']
392+
# UC auto-populates unity.catalog.managed.<format>.defaults.* keys after create.
393+
- field: properties['unity.catalog.managed.*.defaults.*']
400394

401395
schemas:
402396
provided_id_fields:
@@ -412,16 +406,10 @@ resources:
412406
- field: storage_root
413407
reason: uc_strips_trailing_slash
414408
backend_defaults:
415-
# UC auto-populates these system-managed keys after create.
409+
# UC auto-populates unity.catalog.managed.<format>.defaults.* keys after create.
416410
# Without this, every subsequent plan produces an Update whose payload is empty,
417411
# and UC rejects it with "UpdateSchema Nothing to update".
418-
- field: properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking']
419-
- field: properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged']
420-
- field: properties['unity.catalog.managed.delta.defaults.defaultClusterByAuto']
421-
- field: properties['unity.catalog.managed.delta.defaults.delta.checkpointPolicy']
422-
- field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version']
423-
- field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version.afe.internal']
424-
- field: properties['unity.catalog.managed.delta.defaults.delta.feature.catalogManaged']
412+
- field: properties['unity.catalog.managed.*.defaults.*']
425413

426414
external_locations:
427415
recreate_on_changes:

libs/structs/structpath/path.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,8 @@ func (p *PathNode) HasPatternPrefix(prefix *PatternNode) bool {
865865
}
866866

867867
// nodeMatchesPattern checks if a concrete path node matches a pattern node.
868-
// Wildcards (.* and [*]) match any node.
868+
// Wildcards (.* and [*]) match any node. A * inside a bracket map key matches
869+
// one dot-separated segment; a trailing * matches the remainder of the key.
869870
func nodeMatchesPattern(concrete *PathNode, pattern *PatternNode) bool {
870871
if concrete == nil && pattern == nil {
871872
return true
@@ -876,9 +877,37 @@ func nodeMatchesPattern(concrete *PathNode, pattern *PatternNode) bool {
876877
if pattern.DotStar() || pattern.BracketStar() {
877878
return true
878879
}
880+
patternKey, patternIsBracket := ((*PathNode)(pattern)).BracketString()
881+
concreteKey, concreteIsBracket := concrete.BracketString()
882+
if patternIsBracket && concreteIsBracket && strings.Contains(patternKey, "*") {
883+
return matchDotSeparatedGlob(patternKey, concreteKey)
884+
}
879885
return nodesEqual(concrete, (*PathNode)(pattern))
880886
}
881887

888+
// matchDotSeparatedGlob matches keys split on '.'. Each '*' consumes exactly one
889+
// segment unless it is the last pattern segment, in which case it absorbs the
890+
// remaining segments after the preceding '.'. That suffix must be present (a
891+
// key ending in '.' has an empty final segment and still matches), so the key
892+
// needs at least one segment beyond the '*'s position.
893+
func matchDotSeparatedGlob(pattern, key string) bool {
894+
patternParts := strings.Split(pattern, ".")
895+
keyParts := strings.Split(key, ".")
896+
for i, p := range patternParts {
897+
if p == "*" && i == len(patternParts)-1 {
898+
return i < len(keyParts) // trailing '*' absorbs a non-empty suffix
899+
}
900+
if i >= len(keyParts) {
901+
return false // pattern still has segments, key ran out
902+
}
903+
if p != "*" && p != keyParts[i] {
904+
return false
905+
}
906+
}
907+
// Pattern exhausted with no trailing '*': the key must be too.
908+
return len(patternParts) == len(keyParts)
909+
}
910+
882911
// MarshalYAML implements yaml.Marshaler for PathNode.
883912
func (p *PathNode) MarshalYAML() (any, error) {
884913
return p.String(), nil

libs/structs/structpath/path_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,56 @@ func TestHasPatternPrefix(t *testing.T) {
988988
pattern: "a.x.c",
989989
expected: false,
990990
},
991+
992+
// Bracket map key globs
993+
{
994+
name: "bracket map key glob matches managed delta property",
995+
path: "properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking']",
996+
pattern: "properties['unity.catalog.managed.*.defaults.*']",
997+
expected: true,
998+
},
999+
{
1000+
name: "bracket map key glob matches managed iceberg property",
1001+
path: "properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged']",
1002+
pattern: "properties['unity.catalog.managed.*.defaults.*']",
1003+
expected: true,
1004+
},
1005+
{
1006+
name: "bracket map key glob rejects unrelated property",
1007+
path: "properties['custom.remote_only']",
1008+
pattern: "properties['unity.catalog.managed.*.defaults.*']",
1009+
expected: false,
1010+
},
1011+
{
1012+
name: "bracket map key glob rejects mismatched non-wildcard segment",
1013+
path: "properties['unity.catalog.managed.delta.other.delta.enableRowTracking']",
1014+
pattern: "properties['unity.catalog.managed.*.defaults.*']",
1015+
expected: false,
1016+
},
1017+
{
1018+
// A trailing '*' matches the segment(s) after the preceding literal
1019+
// '.'; a key ending in '.' has an empty final segment, which matches.
1020+
name: "bracket map key glob trailing star matches empty child",
1021+
path: "properties['unity.catalog.managed.something.defaults.']",
1022+
pattern: "properties['unity.catalog.managed.something.defaults.*']",
1023+
expected: true,
1024+
},
1025+
{
1026+
// A trailing '*' requires a child segment; a key ending at the
1027+
// preceding literal (no trailing '.') has nothing for '*' to match.
1028+
name: "bracket map key glob trailing star requires a child segment",
1029+
path: "properties['unity.catalog.managed.delta.defaults']",
1030+
pattern: "properties['unity.catalog.managed.delta.defaults.*']",
1031+
expected: false,
1032+
},
1033+
{
1034+
// A non-trailing '*' consumes exactly one segment, so a key missing
1035+
// that segment does not match.
1036+
name: "bracket map key glob middle star requires one segment",
1037+
path: "properties['unity.catalog.managed.defaults.enableRowTracking']",
1038+
pattern: "properties['unity.catalog.managed.*.defaults.enableRowTracking']",
1039+
expected: false,
1040+
},
9911041
}
9921042

9931043
for _, tt := range tests {

0 commit comments

Comments
 (0)