Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/bundles/5877.md
Original file line number Diff line number Diff line change
@@ -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)).
31 changes: 26 additions & 5 deletions bundle/direct/bundle_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
}

Expand All @@ -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",
Expand Down
20 changes: 4 additions & 16 deletions bundle/direct/dresources/resources.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.<format>.defaults.* keys after create.
- field: properties['unity.catalog.managed.*.defaults.*']

schemas:
provided_id_fields:
Expand All @@ -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.<format>.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:
Expand Down
31 changes: 30 additions & 1 deletion libs/structs/structpath/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
50 changes: 50 additions & 0 deletions libs/structs/structpath/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.something.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.delta.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.enableRowTracking']",
expected: false,
},
}

for _, tt := range tests {
Expand Down
Loading