From 2323bab63ab5a3e164d441ee1d7c4bd45b09c7c0 Mon Sep 17 00:00:00 2001 From: Rada Kamysheva Date: Thu, 9 Jul 2026 10:17:45 +0000 Subject: [PATCH 1/4] direct: wildcard-match UC managed property backend defaults Replace per-key backend_defaults entries for catalogs and schemas with a single unity.catalog.managed.*.defaults.* pattern so new Auto Upgrade keys are recognized without listing each one explicitly. --- .nextchanges/bundles/5877.md | 1 + bundle/direct/bundle_plan_test.go | 31 +++++++++++++--- bundle/direct/dresources/resources.yml | 20 +++-------- libs/structs/structpath/path.go | 31 +++++++++++++++- libs/structs/structpath/path_test.go | 50 ++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 .nextchanges/bundles/5877.md diff --git a/.nextchanges/bundles/5877.md b/.nextchanges/bundles/5877.md new file mode 100644 index 00000000000..fb8e0b713a3 --- /dev/null +++ b/.nextchanges/bundles/5877.md @@ -0,0 +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)). diff --git a/bundle/direct/bundle_plan_test.go b/bundle/direct/bundle_plan_test.go index d94fdb3b416..22db70ef62f 100644 --- a/bundle/direct/bundle_plan_test.go +++ b/bundle/direct/bundle_plan_test.go @@ -77,14 +77,11 @@ func TestShouldSkipBackendDefault_ManagedPropertiesOnly(t *testing.T) { // Rules mirror the schemas backend_defaults in resources.yml, but the test is // deliberately self-contained so that edits to resources.yml don't break it. // The real wiring is covered by acceptance/bundle/resources/schemas/drift. - rowTracking, err := structpath.ParsePattern("properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking']") - require.NoError(t, err) - catalogManaged, err := structpath.ParsePattern("properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged']") + managedDefaults, err := structpath.ParsePattern("properties['unity.catalog.managed.*.defaults.*']") require.NoError(t, err) cfg := &dresources.ResourceLifecycleConfig{ BackendDefaults: []dresources.BackendDefaultRule{ - {Field: rowTracking}, - {Field: catalogManaged}, + {Field: managedDefaults}, }, } @@ -106,12 +103,36 @@ func TestShouldSkipBackendDefault_ManagedPropertiesOnly(t *testing.T) { remote: "true", expected: true, }, + { + name: "managed delta cluster by auto property", + path: "properties['unity.catalog.managed.delta.defaults.defaultClusterByAuto']", + remote: "true", + expected: true, + }, + { + name: "managed delta checkpoint policy property", + path: "properties['unity.catalog.managed.delta.defaults.delta.checkpointPolicy']", + remote: "v2", + expected: true, + }, + { + name: "managed delta feature catalogManaged property", + path: "properties['unity.catalog.managed.delta.defaults.delta.feature.catalogManaged']", + remote: "supported", + expected: true, + }, { name: "unmanaged remote-only property is not skipped", path: "properties['custom.remote_only']", remote: "true", expected: false, }, + { + name: "managed prefix without defaults segment is not skipped", + path: "properties['unity.catalog.managed.delta.other.delta.enableRowTracking']", + remote: "true", + expected: false, + }, { name: "managed-only parent properties map is skipped", path: "properties", diff --git a/bundle/direct/dresources/resources.yml b/bundle/direct/dresources/resources.yml index 40e441d0f8f..332b934a286 100644 --- a/bundle/direct/dresources/resources.yml +++ b/bundle/direct/dresources/resources.yml @@ -389,14 +389,8 @@ resources: - field: name reason: id_changes backend_defaults: - # UC auto-populates these system-managed keys after create (same as schemas). - - field: properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking'] - - field: properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged'] - - field: properties['unity.catalog.managed.delta.defaults.defaultClusterByAuto'] - - field: properties['unity.catalog.managed.delta.defaults.delta.checkpointPolicy'] - - field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version'] - - field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version.afe.internal'] - - field: properties['unity.catalog.managed.delta.defaults.delta.feature.catalogManaged'] + # UC auto-populates unity.catalog.managed..defaults.* keys after create. + - field: properties['unity.catalog.managed.*.defaults.*'] schemas: provided_id_fields: @@ -412,16 +406,10 @@ resources: - field: storage_root reason: uc_strips_trailing_slash backend_defaults: - # UC auto-populates these system-managed keys after create. + # UC auto-populates unity.catalog.managed..defaults.* keys after create. # Without this, every subsequent plan produces an Update whose payload is empty, # and UC rejects it with "UpdateSchema Nothing to update". - - field: properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking'] - - field: properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged'] - - field: properties['unity.catalog.managed.delta.defaults.defaultClusterByAuto'] - - field: properties['unity.catalog.managed.delta.defaults.delta.checkpointPolicy'] - - field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version'] - - field: properties['unity.catalog.managed.delta.defaults.delta.parquet.format.version.afe.internal'] - - field: properties['unity.catalog.managed.delta.defaults.delta.feature.catalogManaged'] + - field: properties['unity.catalog.managed.*.defaults.*'] external_locations: recreate_on_changes: diff --git a/libs/structs/structpath/path.go b/libs/structs/structpath/path.go index 5ae81019ee4..92715dc09d6 100644 --- a/libs/structs/structpath/path.go +++ b/libs/structs/structpath/path.go @@ -865,7 +865,8 @@ func (p *PathNode) HasPatternPrefix(prefix *PatternNode) bool { } // nodeMatchesPattern checks if a concrete path node matches a pattern node. -// Wildcards (.* and [*]) match any node. +// Wildcards (.* and [*]) match any node. A * inside a bracket map key matches +// one dot-separated segment; a trailing * matches the remainder of the key. func nodeMatchesPattern(concrete *PathNode, pattern *PatternNode) bool { if concrete == nil && pattern == nil { return true @@ -876,9 +877,37 @@ func nodeMatchesPattern(concrete *PathNode, pattern *PatternNode) bool { if pattern.DotStar() || pattern.BracketStar() { return true } + patternKey, patternIsBracket := ((*PathNode)(pattern)).BracketString() + concreteKey, concreteIsBracket := concrete.BracketString() + if patternIsBracket && concreteIsBracket && strings.Contains(patternKey, "*") { + return matchDotSeparatedGlob(patternKey, concreteKey) + } return nodesEqual(concrete, (*PathNode)(pattern)) } +// matchDotSeparatedGlob matches keys split on '.'. Each '*' consumes exactly one +// segment unless it is the last pattern segment, in which case it absorbs the +// remaining segments after the preceding '.'. That suffix must be present (a +// key ending in '.' has an empty final segment and still matches), so the key +// needs at least one segment beyond the '*'s position. +func matchDotSeparatedGlob(pattern, key string) bool { + patternParts := strings.Split(pattern, ".") + keyParts := strings.Split(key, ".") + for i, p := range patternParts { + if p == "*" && i == len(patternParts)-1 { + return i < len(keyParts) // trailing '*' absorbs a non-empty suffix + } + if i >= len(keyParts) { + return false // pattern still has segments, key ran out + } + if p != "*" && p != keyParts[i] { + return false + } + } + // Pattern exhausted with no trailing '*': the key must be too. + return len(patternParts) == len(keyParts) +} + // MarshalYAML implements yaml.Marshaler for PathNode. func (p *PathNode) MarshalYAML() (any, error) { return p.String(), nil diff --git a/libs/structs/structpath/path_test.go b/libs/structs/structpath/path_test.go index 2f8fe4bf6be..b23bd6ff84b 100644 --- a/libs/structs/structpath/path_test.go +++ b/libs/structs/structpath/path_test.go @@ -988,6 +988,56 @@ func TestHasPatternPrefix(t *testing.T) { pattern: "a.x.c", expected: false, }, + + // Bracket map key globs + { + name: "bracket map key glob matches managed delta property", + path: "properties['unity.catalog.managed.delta.defaults.delta.enableRowTracking']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: true, + }, + { + name: "bracket map key glob matches managed iceberg property", + path: "properties['unity.catalog.managed.iceberg.defaults.delta.feature.catalogManaged']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: true, + }, + { + name: "bracket map key glob rejects unrelated property", + path: "properties['custom.remote_only']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: false, + }, + { + name: "bracket map key glob rejects mismatched non-wildcard segment", + path: "properties['unity.catalog.managed.delta.other.delta.enableRowTracking']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: false, + }, + { + // A trailing '*' matches the segment(s) after the preceding literal + // '.'; a key ending in '.' has an empty final segment, which matches. + name: "bracket map key glob trailing star matches empty child", + path: "properties['unity.catalog.managed.something.defaults.']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: true, + }, + { + // A trailing '*' requires a child segment; a key ending at the + // preceding literal (no trailing '.') has nothing for '*' to match. + name: "bracket map key glob trailing star requires a child segment", + path: "properties['unity.catalog.managed.delta.defaults']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: false, + }, + { + // A non-trailing '*' consumes exactly one segment, so a key missing + // that segment does not match. + name: "bracket map key glob middle star requires one segment", + path: "properties['unity.catalog.managed.defaults.enableRowTracking']", + pattern: "properties['unity.catalog.managed.*.defaults.*']", + expected: false, + }, } for _, tt := range tests { From 17c117fcc3293ee31a409265d4db188a40c5af0c Mon Sep 17 00:00:00 2001 From: radakam <55745584+radakam@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:47:32 +0200 Subject: [PATCH 2/4] Update libs/structs/structpath/path_test.go Co-authored-by: Jan N Rose --- libs/structs/structpath/path_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/structs/structpath/path_test.go b/libs/structs/structpath/path_test.go index b23bd6ff84b..bb3fb96dcfe 100644 --- a/libs/structs/structpath/path_test.go +++ b/libs/structs/structpath/path_test.go @@ -1019,7 +1019,7 @@ func TestHasPatternPrefix(t *testing.T) { // '.'; a key ending in '.' has an empty final segment, which matches. name: "bracket map key glob trailing star matches empty child", path: "properties['unity.catalog.managed.something.defaults.']", - pattern: "properties['unity.catalog.managed.*.defaults.*']", + pattern: "properties['unity.catalog.managed.something.defaults.*']", expected: true, }, { From 30666264f90e5d23e1045ae4bacaec547098d3d7 Mon Sep 17 00:00:00 2001 From: radakam <55745584+radakam@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:47:46 +0200 Subject: [PATCH 3/4] Update libs/structs/structpath/path_test.go Co-authored-by: Jan N Rose --- libs/structs/structpath/path_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/structs/structpath/path_test.go b/libs/structs/structpath/path_test.go index bb3fb96dcfe..098054b88a4 100644 --- a/libs/structs/structpath/path_test.go +++ b/libs/structs/structpath/path_test.go @@ -1027,7 +1027,7 @@ func TestHasPatternPrefix(t *testing.T) { // preceding literal (no trailing '.') has nothing for '*' to match. name: "bracket map key glob trailing star requires a child segment", path: "properties['unity.catalog.managed.delta.defaults']", - pattern: "properties['unity.catalog.managed.*.defaults.*']", + pattern: "properties['unity.catalog.managed.delta.defaults.*']", expected: false, }, { From dae98af4a37b6c57a9b1ef51f13784db91a2c191 Mon Sep 17 00:00:00 2001 From: radakam <55745584+radakam@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:47:55 +0200 Subject: [PATCH 4/4] Update libs/structs/structpath/path_test.go Co-authored-by: Jan N Rose --- libs/structs/structpath/path_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/structs/structpath/path_test.go b/libs/structs/structpath/path_test.go index 098054b88a4..b8623db76d0 100644 --- a/libs/structs/structpath/path_test.go +++ b/libs/structs/structpath/path_test.go @@ -1035,7 +1035,7 @@ func TestHasPatternPrefix(t *testing.T) { // that segment does not match. name: "bracket map key glob middle star requires one segment", path: "properties['unity.catalog.managed.defaults.enableRowTracking']", - pattern: "properties['unity.catalog.managed.*.defaults.*']", + pattern: "properties['unity.catalog.managed.*.defaults.enableRowTracking']", expected: false, }, }