diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0f71834ff56..72c22534455 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -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 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..0aca174075e 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,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 diff --git a/libs/structs/structpath/path_test.go b/libs/structs/structpath/path_test.go index 2f8fe4bf6be..8635d9d4cf8 100644 --- a/libs/structs/structpath/path_test.go +++ b/libs/structs/structpath/path_test.go @@ -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 {