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<>();

Reply via email to