This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new a49db404cd [#13719][followup] fix(iceberg): Ignore identity masks
during conflict detection (#14384)
a49db404cd is described below
commit a49db404cd8427e1ffae5311acc04fabed05fd23
Author: roryqi <[email protected]>
AuthorDate: Wed Nov 4 14:04:15 2026 +0800
[#13719][followup] fix(iceberg): Ignore identity masks during conflict
detection (#14384)
### What changes were proposed in this pull request?
Skip empty resolved column-mask restrictions before deduplication and
conflict detection, matching the row-filter behavior. Add four
regression tests covering identity masks, conditional branches, policy
ordering, and type validation.
### Why are the changes needed?
A policy returning the original value produces no projection, but its
empty restriction currently conflicts with another policy's effective
mask on the same field and rejects the table load.
### Does this PR introduce _any_ user-facing change?
Yes. Identity masks can coexist with an effective mask without rejecting
the table load. Distinct effective masks still conflict, and invalid
mask types still fail validation.
### How was this patch tested?
The two new mixed-mask regression tests reproduced the conflict before
the fix. All 58 experimental Iceberg REST module tests pass after the
fix.
```bash
./gradlew :iceberg:iceberg-rest-server-experiment:spotlessApply
:iceberg:iceberg-rest-server-experiment:test -PskipITs
```
---
.../ExperimentalIcebergReadRestrictionLoader.java | 3 +
...stExperimentalIcebergReadRestrictionLoader.java | 94 ++++++++++++++++++++++
2 files changed, 97 insertions(+)
diff --git
a/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
b/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
index e02c73af61..708d4c7ef7 100644
---
a/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
+++
b/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
@@ -223,6 +223,9 @@ public final class ExperimentalIcebergReadRestrictionLoader
Collections.singletonMap(fieldId, program),
schema,
bindingContext);
+ if (resolved.isEmpty()) {
+ continue;
+ }
distinct.putIfAbsent(ReadRestrictionsParser.toJson(resolved), program);
}
if (distinct.size() > 1) {
diff --git
a/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
b/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
index 78d4096df7..42612ca341 100644
---
a/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
+++
b/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
@@ -541,6 +541,100 @@ public class TestExperimentalIcebergReadRestrictionLoader
{
assertFalse(inconsistent.isPresent());
}
+ @Test
+ void testIgnoresIdentityMasksAlongsideEffectiveMasks() {
+ PolicyEntity identity = maskPolicy(1, "identity", "(value STRING) RETURNS
STRING -> value");
+ PolicyEntity mask =
+ maskPolicy(2, "masked", "(value STRING) RETURNS STRING ->
show_last_4(value)");
+ Map<Integer, PolicyEntity[]> policies = new LinkedHashMap<>();
+ policies.put(2, new PolicyEntity[] {identity, mask});
+ policies.put(3, new PolicyEntity[] {mask, identity});
+
+ ReadRestrictions restrictions =
+ ExperimentalIcebergReadRestrictionLoader.resolve(
+ new PolicyEntity[0], policies, SCHEMA, context("alice"));
+
+ assertEquals(2, restrictions.columnProjections().size());
+ assertEquals(2, restrictions.columnProjections().get(0).fieldId());
+ assertEquals(3, restrictions.columnProjections().get(1).fieldId());
+ assertEquals("show-last-4",
restrictions.columnProjections().get(0).name());
+ assertEquals("show-last-4",
restrictions.columnProjections().get(1).name());
+ }
+
+ @Test
+ void testConditionalIdentityMaskDoesNotConflictWithEffectiveMask() {
+ Map<Integer, PolicyEntity[]> policies =
+ Collections.singletonMap(
+ 2,
+ new PolicyEntity[] {
+ maskPolicy(
+ 1,
+ "conditional",
+ "(value STRING) RETURNS STRING -> CASE WHEN
is_group_member('auditors') THEN show_first_4(value) ELSE value END"),
+ maskPolicy(2, "masked", "(value STRING) RETURNS STRING ->
show_last_4(value)")
+ });
+
+ ReadRestrictions restrictions =
+ ExperimentalIcebergReadRestrictionLoader.resolve(
+ new PolicyEntity[0],
+ policies,
+ SCHEMA,
+ context("alice", Collections.singletonMap("auditors", false)));
+
+ assertEquals(1, restrictions.columnProjections().size());
+ assertEquals(2, restrictions.columnProjections().get(0).fieldId());
+ assertEquals("show-last-4",
restrictions.columnProjections().get(0).name());
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ExperimentalIcebergReadRestrictionLoader.resolve(
+ new PolicyEntity[0],
+ policies,
+ SCHEMA,
+ context("alice", Collections.singletonMap("auditors",
true))));
+ assertTrue(exception.getMessage().contains("Conflicting column-mask"));
+ }
+
+ @Test
+ void testAllIdentityMasksAreUnrestricted() {
+ Map<Integer, PolicyEntity[]> policies =
+ Collections.singletonMap(
+ 2,
+ new PolicyEntity[] {
+ maskPolicy(1, "identity", "(value STRING) RETURNS STRING ->
value"),
+ maskPolicy(
+ 2,
+ "conditional",
+ "(value STRING) RETURNS STRING -> CASE WHEN
is_group_member('auditors') THEN show_last_4(value) ELSE value END")
+ });
+
+ ReadRestrictions restrictions =
+ ExperimentalIcebergReadRestrictionLoader.resolve(
+ new PolicyEntity[0],
+ policies,
+ SCHEMA,
+ context("alice", Collections.singletonMap("auditors", false)));
+
+ assertTrue(restrictions.isEmpty());
+ }
+
+ @Test
+ void testRejectsIdentityMaskWithIncompatibleFieldType() {
+ Map<Integer, PolicyEntity[]> policies =
+ Collections.singletonMap(
+ 1,
+ new PolicyEntity[] {
+ maskPolicy(1, "identity", "(value STRING) RETURNS STRING ->
value")
+ });
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ExperimentalIcebergReadRestrictionLoader.resolve(
+ new PolicyEntity[0], policies, SCHEMA, context("alice")));
+ }
+
@Test
void testRejectsDistinctMasksForOneField() {
Map<Integer, PolicyEntity[]> policies = new LinkedHashMap<>();