Skip to content
Open
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
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bundles

* direct: Match UC Auto Upgrade managed property defaults with a wildcard pattern instead of enumerating each key.

### Dependency updates

### API Changes
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
33 changes: 32 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,39 @@ 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 matches any suffix.
func matchDotSeparatedGlob(pattern, key string) bool {
return matchDotSeparatedGlobParts(strings.Split(pattern, "."), strings.Split(key, "."))
}

func matchDotSeparatedGlobParts(patternParts, keyParts []string) bool {
if len(patternParts) == 0 {
return len(keyParts) == 0
}
if patternParts[0] == "*" {
if len(patternParts) == 1 {
return true
}
if len(keyParts) == 0 {
return false
}
return matchDotSeparatedGlobParts(patternParts[1:], keyParts[1:])
}
if len(keyParts) == 0 || patternParts[0] != keyParts[0] {
return false
}
return matchDotSeparatedGlobParts(patternParts[1:], keyParts[1:])
}

// MarshalYAML implements yaml.Marshaler for PathNode.
func (p *PathNode) MarshalYAML() (any, error) {
return p.String(), nil
Expand Down
26 changes: 26 additions & 0 deletions libs/structs/structpath/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,32 @@ 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 managed key without defaults segment",
path: "properties['unity.catalog.managed.delta.other.delta.enableRowTracking']",
pattern: "properties['unity.catalog.managed.*.defaults.*']",
expected: false,
},
}

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