You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by cs...@apache.org on 2026/10/05 10:29:37 UTC

(impala) branch master updated: IMPALA-15365: Iceberg UUID partition transform support

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git


The following commit(s) were added to refs/heads/master by this push:
     new 2e8fb1d8b IMPALA-15365: Iceberg UUID partition transform support
2e8fb1d8b is described below

commit 2e8fb1d8b7ec3ecbfe8878a03b6dcf2af942e785
Author: Arnab Karmakar <ar...@gmail.com>
AuthorDate: Mon Sep 28 03:47:05 2026 -0700

    IMPALA-15365: Iceberg UUID partition transform support
    
    Adds partition transform support for the Iceberg UUID type. The Iceberg
    spec only allows the identity, bucket(N, col) and void partition
    transforms on UUID columns. Impala now rejects the other transforms
    (year, month, day, hour and truncate) at analysis time for CREATE TABLE
    and ALTER TABLE ... SET PARTITION SPEC, instead of failing later in
    Iceberg.
    
    The Iceberg Java library compares UUIDs with the signed
    java.util.UUID.compareTo(), while Parquet writes UUID min/max bounds in
    unsigned byte order. This is a bug in the Java library, not in the
    Iceberg spec, which stores UUIDs as 16 big-endian bytes. Because of it,
    Iceberg's metrics evaluation can skip data files that contain matching
    rows. UUID value predicates are therefore not pushed to Iceberg and are
    evaluated by the scan. IS NULL and IS NOT NULL are still pushed.
    
    DROP PARTITION, SHOW FILES ... PARTITION and SHOW PARTITIONS ... WHERE
    accept =, !=, IN and NOT IN on UUID columns that are identity
    partitioned in every partition spec. Iceberg matches these values
    exactly against the partition values, so byte order does not matter.
    For = and IN, Iceberg also prunes manifests by their partition summary
    bounds. This assumes the summaries were written by the Iceberg Java
    library (Impala, Trino, Spark), which builds them with the same signed
    comparison. Range comparisons on UUID columns are rejected in these
    statements.
    
    Testing:
    - FE tests: identity, bucket and void are accepted on UUID columns and
      the other transforms are rejected, for CREATE TABLE and SET PARTITION
      SPEC.
    - e2e tests on the new iceberg_uuid_test_part fixture for reads, scan
      predicates, EXPLAIN, DROP PARTITION, SHOW FILES and SHOW PARTITIONS,
      including =, !=, IN and NOT IN.
    - Trino interop tests for partition evolution from identity to bucket
      and for DROP PARTITION on Trino-written identity partitions.
    - Added the iceberg_uuid_test_part fixture (Iceberg V3,
      identity(uuid_identity), bucket(4, uuid_bucket)), generated with the
      Iceberg Java API (version 1.11.0). One data file holds values on both
      sides of the sign bit. All data files are added in a single append,
      so one manifest's uuid_identity summary spans the sign bit.
    
    Change-Id: I1dc3af4418285d6ea1d489912419be470e668f29
    Assisted-by: Claude Opus 5.5 (Cursor)
    Reviewed-on: http://gerrit.cloudera.org:8080/24973
    Reviewed-by: Impala Public Jenkins <im...@cloudera.com>
    Tested-by: Impala Public Jenkins <im...@cloudera.com>
---
 .../analysis/AlterTableDropPartitionStmt.java      |   2 +-
 .../analysis/AlterTableSetPartitionSpecStmt.java   |   1 +
 .../apache/impala/analysis/CreateTableStmt.java    |   7 +-
 .../impala/analysis/IcebergPartitionField.java     |  17 ++
 .../org/apache/impala/analysis/ShowFilesStmt.java  |   2 +-
 .../org/apache/impala/analysis/ShowStatsStmt.java  |   2 +-
 .../common/IcebergPartitionPredicateConverter.java |  40 ++-
 .../impala/common/IcebergPredicateConverter.java   |  26 +-
 .../java/org/apache/impala/util/IcebergUtil.java   |   8 +
 .../apache/impala/analysis/AnalyzeStmtsTest.java   |  42 ++++
 testdata/data/README                               |  20 ++
 ...22e-d632-4420-ac01-8b1e90eb07a0-1-00001.parquet | Bin 0 -> 1083 bytes
 ...792-853c-495b-a239-86f69d1d8e38-1-00001.parquet | Bin 0 -> 1059 bytes
 ...c37-060a-4877-acd7-40c9e162917c-1-00001.parquet | Bin 0 -> 1083 bytes
 ...c42-347b-4eea-98c7-ad80a9c97275-1-00001.parquet | Bin 0 -> 871 bytes
 .../4757457c-ce31-46a5-aa84-18521e2a2903-m0.avro   | Bin 0 -> 8685 bytes
 ...296-1-4757457c-ce31-46a5-aa84-18521e2a2903.avro | Bin 0 -> 4764 bytes
 .../metadata/v1.metadata.json                      |   1 +
 .../metadata/v2.metadata.json                      |   1 +
 .../metadata/version-hint.text                     |   1 +
 .../QueryTest/iceberg-trino-interop-uuid.test      | 112 +++++++++
 .../queries/QueryTest/iceberg-uuid-type.test       | 276 +++++++++++++++++++++
 tests/query_test/test_iceberg.py                   |  14 +-
 23 files changed, 554 insertions(+), 18 deletions(-)

diff --git a/fe/src/main/java/org/apache/impala/analysis/AlterTableDropPartitionStmt.java b/fe/src/main/java/org/apache/impala/analysis/AlterTableDropPartitionStmt.java
index 192fc328b..e41f7633a 100644
--- a/fe/src/main/java/org/apache/impala/analysis/AlterTableDropPartitionStmt.java
+++ b/fe/src/main/java/org/apache/impala/analysis/AlterTableDropPartitionStmt.java
@@ -149,7 +149,7 @@ public class AlterTableDropPartitionStmt extends AlterTableStmt {
             table.getIcebergApiTable().spec());
     // For Impala expression to Iceberg expression conversion
     IcebergPredicateConverter converter =
-        new IcebergPartitionPredicateConverter(table.getIcebergSchema(), analyzer);
+        new IcebergPartitionPredicateConverter(table, analyzer);
 
     List<Expression> icebergPartitionExprs = new ArrayList<>();
     for (Expr expr : partitionSet_.getPartitionExprs()) {
diff --git a/fe/src/main/java/org/apache/impala/analysis/AlterTableSetPartitionSpecStmt.java b/fe/src/main/java/org/apache/impala/analysis/AlterTableSetPartitionSpecStmt.java
index b2cbf436c..4c683a779 100644
--- a/fe/src/main/java/org/apache/impala/analysis/AlterTableSetPartitionSpecStmt.java
+++ b/fe/src/main/java/org/apache/impala/analysis/AlterTableSetPartitionSpecStmt.java
@@ -91,6 +91,7 @@ public class AlterTableSetPartitionSpecStmt extends AlterTableStmt {
             "Source column '%s' in table %s must be a primitive type",
             partField.getFieldName(), table.getTableName()));
       }
+      partField.analyzeSourceType(col.getType());
     }
   }
 }
diff --git a/fe/src/main/java/org/apache/impala/analysis/CreateTableStmt.java b/fe/src/main/java/org/apache/impala/analysis/CreateTableStmt.java
index 3a381cf42..089830bcc 100644
--- a/fe/src/main/java/org/apache/impala/analysis/CreateTableStmt.java
+++ b/fe/src/main/java/org/apache/impala/analysis/CreateTableStmt.java
@@ -863,16 +863,17 @@ public class CreateTableStmt extends StatementBase implements SingleTableStmt {
     Preconditions.checkState(fields != null && !fields.isEmpty());
     for (IcebergPartitionField field : fields) {
       String fieldName = field.getFieldName();
-      boolean containFlag = false;
+      ColumnDef sourceColumnDef = null;
       for (ColumnDef columnDef : tableDef_.getColumnDefs()) {
         if (columnDef.getColName().equalsIgnoreCase(fieldName)) {
-          containFlag = true;
+          sourceColumnDef = columnDef;
           break;
         }
       }
-      if (!containFlag) {
+      if (sourceColumnDef == null) {
         throw new AnalysisException("Cannot find source column: " + fieldName);
       }
+      field.analyzeSourceType(sourceColumnDef.getType());
     }
   }
 
diff --git a/fe/src/main/java/org/apache/impala/analysis/IcebergPartitionField.java b/fe/src/main/java/org/apache/impala/analysis/IcebergPartitionField.java
index ec7a4c641..8bbc597b8 100644
--- a/fe/src/main/java/org/apache/impala/analysis/IcebergPartitionField.java
+++ b/fe/src/main/java/org/apache/impala/analysis/IcebergPartitionField.java
@@ -92,6 +92,23 @@ public class IcebergPartitionField extends StmtNode {
     transform_.analyze(analyzer);
   }
 
+  /**
+   * Checks that this field's transform can be applied to a source column of type
+   * 'sourceType'. UUID source columns support IDENTITY, BUCKET, and VOID transforms.
+   */
+  public void analyzeSourceType(Type sourceType) throws AnalysisException {
+    if (!sourceType.isUuid()) return;
+    TIcebergPartitionTransformType transformType = getTransformType();
+    if (transformType == TIcebergPartitionTransformType.IDENTITY
+        || transformType == TIcebergPartitionTransformType.BUCKET
+        || transformType == TIcebergPartitionTransformType.VOID) {
+      return;
+    }
+    throw new AnalysisException(String.format("Partition transform %s is not " +
+        "supported on UUID column '%s'. Only IDENTITY, BUCKET, and VOID transforms " +
+        "are supported on UUID columns.", transformType, origFieldName_));
+  }
+
   @Override
   public final String toSql() {
     return toSql(ToSqlOptions.DEFAULT);
diff --git a/fe/src/main/java/org/apache/impala/analysis/ShowFilesStmt.java b/fe/src/main/java/org/apache/impala/analysis/ShowFilesStmt.java
index 971f80685..50795da55 100644
--- a/fe/src/main/java/org/apache/impala/analysis/ShowFilesStmt.java
+++ b/fe/src/main/java/org/apache/impala/analysis/ShowFilesStmt.java
@@ -161,7 +161,7 @@ public class ShowFilesStmt extends StatementBase implements SingleTableStmt {
               table.getIcebergApiTable().spec());
       // For Impala expression to Iceberg expression conversion
       IcebergPredicateConverter converter =
-          new IcebergPartitionPredicateConverter(table.getIcebergSchema(), analyzer);
+          new IcebergPartitionPredicateConverter(table, analyzer);
 
       for (Expr expr : partitionSet_.getPartitionExprs()) {
         expr = rewriter.rewrite(expr);
diff --git a/fe/src/main/java/org/apache/impala/analysis/ShowStatsStmt.java b/fe/src/main/java/org/apache/impala/analysis/ShowStatsStmt.java
index f6f746333..24fb0fd45 100644
--- a/fe/src/main/java/org/apache/impala/analysis/ShowStatsStmt.java
+++ b/fe/src/main/java/org/apache/impala/analysis/ShowStatsStmt.java
@@ -291,7 +291,7 @@ public class ShowStatsStmt extends StatementBase implements SingleTableStmt {
       // Convert the Impala expression to an Iceberg expression
       // BoolLiterals are handled by the converter and optimized in getPartitionStats
       IcebergPartitionPredicateConverter converter =
-          new IcebergPartitionPredicateConverter(table.getIcebergSchema(), analyzer);
+          new IcebergPartitionPredicateConverter(table, analyzer);
       IcebergPredicateConverter.ConverterResult result = converter.convert(foldedExpr);
 
       if (result.isFailed()) {
diff --git a/fe/src/main/java/org/apache/impala/common/IcebergPartitionPredicateConverter.java b/fe/src/main/java/org/apache/impala/common/IcebergPartitionPredicateConverter.java
index 1a7a3cf93..69432698d 100644
--- a/fe/src/main/java/org/apache/impala/common/IcebergPartitionPredicateConverter.java
+++ b/fe/src/main/java/org/apache/impala/common/IcebergPartitionPredicateConverter.java
@@ -17,20 +17,28 @@
 
 package org.apache.impala.common;
 
-import org.apache.iceberg.Schema;
+import java.util.UUID;
+import org.apache.iceberg.expressions.Expression.Operation;
 import org.apache.iceberg.expressions.Expressions;
+import org.apache.iceberg.expressions.NamedReference;
 import org.apache.iceberg.expressions.UnboundTerm;
 import org.apache.iceberg.transforms.Transforms;
 import org.apache.impala.analysis.Analyzer;
 import org.apache.impala.analysis.Expr;
 import org.apache.impala.analysis.IcebergPartitionExpr;
+import org.apache.impala.analysis.StringLiteral;
 import org.apache.impala.catalog.Column;
+import org.apache.impala.catalog.FeIcebergTable;
 import org.apache.impala.catalog.IcebergColumn;
 import org.apache.impala.thrift.TIcebergPartitionTransformType;
+import org.apache.impala.util.IcebergUtil;
 
 public class IcebergPartitionPredicateConverter extends IcebergPredicateConverter {
-  public IcebergPartitionPredicateConverter(Schema schema, Analyzer analyzer) {
-    super(schema, analyzer);
+  private final FeIcebergTable table_;
+
+  public IcebergPartitionPredicateConverter(FeIcebergTable table, Analyzer analyzer) {
+    super(table.getIcebergSchema(), analyzer);
+    table_ = table;
   }
 
   @Override
@@ -48,9 +56,33 @@ public class IcebergPartitionPredicateConverter extends IcebergPredicateConverte
     IcebergColumn icebergColumn = (IcebergColumn) column;
     if (partitionExpr.getTransform().getTransformType().equals(
         TIcebergPartitionTransformType.IDENTITY)) {
-      return new Term(Expressions.ref(column.getName()),icebergColumn);
+      return new Term(Expressions.ref(column.getName()), icebergColumn);
     }
     return new Term((UnboundTerm<Object>) Expressions.transform(column.getName(),
         Transforms.fromString(partitionExpr.getTransform().toSql())), icebergColumn);
   }
+
+  @Override
+  protected Object getIcebergUuidValue(Term term, Operation op, StringLiteral literal)
+      throws ImpalaRuntimeException {
+    // Safe for (in)equality on a field that is identity-partitioned in every spec.
+    // Iceberg evaluates the filter exactly on each file's partition value, and equality
+    // does not depend on byte order. Data file bounds are equal within an identity
+    // partition. != and NOT IN only prune manifests whose summary holds a single value.
+    // = and IN also prune manifests by their partition summary bounds, which assumes the
+    // summaries were built with the Iceberg Java library's signed comparator. This holds
+    // for writers that use the library (Impala, Trino, Spark). A writer that orders UUIDs
+    // unsigned (e.g. PyIceberg's min()/max()) would produce summaries that make
+    // ManifestEvaluator skip manifests with matching partitions.
+    boolean isIdentityTerm = term.term_ instanceof NamedReference;
+    boolean isEqualityOp = op == Operation.EQ || op == Operation.NOT_EQ
+        || op == Operation.IN || op == Operation.NOT_IN;
+    if (isIdentityTerm && isEqualityOp
+        && IcebergUtil.isIdentityPartitionedInAllSpecs(table_, term.referencedColumn_)) {
+      return UUID.fromString(literal.getUnescapedValue());
+    }
+    throw new ImpalaRuntimeException(
+        "UUID values can only be used with =, !=, IN or NOT IN on columns that are " +
+        "identity partitioned in every partition spec");
+  }
 }
diff --git a/fe/src/main/java/org/apache/impala/common/IcebergPredicateConverter.java b/fe/src/main/java/org/apache/impala/common/IcebergPredicateConverter.java
index 34b0a2d37..63e655fd6 100644
--- a/fe/src/main/java/org/apache/impala/common/IcebergPredicateConverter.java
+++ b/fe/src/main/java/org/apache/impala/common/IcebergPredicateConverter.java
@@ -152,12 +152,11 @@ public class IcebergPredicateConverter {
   protected ConverterResult convert(BinaryPredicate predicate) {
     try {
       Term term = getTerm(predicate.getChild(0));
-      IcebergColumn column = term.referencedColumn_;
 
       LiteralExpr literal = getSecondChildAsLiteralExpr(predicate);
       checkNullLiteral(literal);
       Operation op = getOperation(predicate);
-      Object value = getIcebergValue(column, literal);
+      Object value = getIcebergValue(term, op, literal);
 
       List<Object> literals = Collections.singletonList(value);
       Expression iceExpr = Expressions.predicate(op, term.term_, literals);
@@ -170,9 +169,9 @@ public class IcebergPredicateConverter {
   protected ConverterResult convert(InPredicate predicate) {
     try {
       Term term = getTerm(predicate.getChild(0));
-      IcebergColumn column = term.referencedColumn_;
       // Expressions takes a list of values as Objects
       List<Object> values = new ArrayList<>();
+      Operation op = predicate.isNotIn() ? Operation.NOT_IN : Operation.IN;
       for (int i = 1; i < predicate.getChildren().size(); ++i) {
         if (!Expr.IS_LITERAL.apply(predicate.getChild(i))) {
           return new ConverterResult(ConversionStatus.FAILED,
@@ -181,7 +180,7 @@ public class IcebergPredicateConverter {
         }
         LiteralExpr literal = (LiteralExpr) predicate.getChild(i);
         checkNullLiteral(literal);
-        Object value = getIcebergValue(column, literal);
+        Object value = getIcebergValue(term, op, literal);
         values.add(value);
       }
 
@@ -437,8 +436,9 @@ public class IcebergPredicateConverter {
     }
   }
 
-  protected Object getIcebergValue(IcebergColumn column, LiteralExpr literal)
+  protected Object getIcebergValue(Term term, Operation op, LiteralExpr literal)
       throws ImpalaRuntimeException {
+    IcebergColumn column = term.referencedColumn_;
     PrimitiveType primitiveType = literal.getType().getPrimitiveType();
     switch (primitiveType) {
       case BOOLEAN: return ((BoolLiteral) literal).getValue();
@@ -448,6 +448,7 @@ public class IcebergPredicateConverter {
       case BIGINT: return ((NumericLiteral) literal).getLongValue();
       case FLOAT: return (float) ((NumericLiteral) literal).getDoubleValue();
       case DOUBLE: return ((NumericLiteral) literal).getDoubleValue();
+      case UUID: return getIcebergUuidValue(term, op, (StringLiteral) literal);
       case STRING:
       case DATETIME:
       case CHAR: return ((StringLiteral) literal).getUnescapedValue();
@@ -465,6 +466,21 @@ public class IcebergPredicateConverter {
     }
   }
 
+  /**
+   * Returns the Iceberg value for a UUID literal. Subclasses may override this to allow
+   * UUID value conversion in contexts where it is safe, such as metadata-only operations.
+   */
+  protected Object getIcebergUuidValue(Term term, Operation op, StringLiteral literal)
+      throws ImpalaRuntimeException {
+    // The Iceberg Java library compares UUIDs as signed longs, but Parquet min/max bounds
+    // use unsigned byte order, so Iceberg could skip data files that contain matching
+    // rows. This is a bug in the Java library, not in the Iceberg spec.
+    // TODO: IMPALA-15476: push down = and IN on columns that are identity partitioned in
+    // every spec, and derived bucket(N, col) = <hash> predicates, which are safe.
+    throw new ImpalaRuntimeException(
+        "UUID values cannot be converted to Iceberg expressions");
+  }
+
   /**
    * Returns Iceberg operator by BinaryPredicate operator, or null if the operation is not
    * supported by Iceberg.
diff --git a/fe/src/main/java/org/apache/impala/util/IcebergUtil.java b/fe/src/main/java/org/apache/impala/util/IcebergUtil.java
index 32323679f..3c454ad2f 100644
--- a/fe/src/main/java/org/apache/impala/util/IcebergUtil.java
+++ b/fe/src/main/java/org/apache/impala/util/IcebergUtil.java
@@ -2135,6 +2135,14 @@ public class IcebergUtil {
     //                           files  files
     // Later PARTITION KEY SCAN could be a UNION NODE that produces the partition keys,
     // see SingleNodePlanner.createOptimizedPartitionUnionNode().
+    return isIdentityPartitionedInAllSpecs(table, column);
+  }
+
+  /**
+   * Returns true if 'column' is identity partitioned in every partition spec of 'table'.
+   */
+  public static boolean isIdentityPartitionedInAllSpecs(FeIcebergTable table,
+      IcebergColumn column) {
     for (IcebergPartitionSpec spec : table.getPartitionSpecs()) {
       if (IcebergUtil.getPartitionTransformType(column, spec) !=
           TIcebergPartitionTransformType.IDENTITY) {
diff --git a/fe/src/test/java/org/apache/impala/analysis/AnalyzeStmtsTest.java b/fe/src/test/java/org/apache/impala/analysis/AnalyzeStmtsTest.java
index f47f8b6c5..e2a465e72 100644
--- a/fe/src/test/java/org/apache/impala/analysis/AnalyzeStmtsTest.java
+++ b/fe/src/test/java/org/apache/impala/analysis/AnalyzeStmtsTest.java
@@ -5283,6 +5283,48 @@ public class AnalyzeStmtsTest extends AnalyzerTest {
         "PARTITIONED BY SPEC (BUCKET(10, p1), DAY(10, p2)) STORED AS ICEBERG" +
         tblProperties,
         "Only BUCKET and TRUNCATE partition transforms accept a parameter.");
+
+    // UUID source columns support IDENTITY, BUCKET, and VOID transforms.
+    AnalyzesOk("CREATE TABLE tbl1 (i int, u uuid) PARTITIONED BY SPEC (u) " +
+        "STORED AS ICEBERG" + tblProperties);
+    AnalyzesOk("CREATE TABLE tbl1 (i int, u uuid) PARTITIONED BY SPEC (BUCKET(4, u)) " +
+        "STORED AS ICEBERG" + tblProperties);
+    AnalyzesOk("CREATE TABLE tbl1 (i int, u uuid) PARTITIONED BY SPEC (VOID(u)) " +
+        "STORED AS ICEBERG" + tblProperties);
+    AnalyzesOk("CREATE TABLE tbl1 (i int, u uuid) " +
+        "PARTITIONED BY SPEC (u, BUCKET(4, u), TRUNCATE(4, i)) " +
+        "STORED AS ICEBERG" + tblProperties);
+    String[][] rejectedUuidTransforms = new String[][] {
+        {"YEAR(u)", "YEAR"}, {"MONTH(u)", "MONTH"}, {"DAY(u)", "DAY"},
+        {"HOUR(u)", "HOUR"}, {"TRUNCATE(4, u)", "TRUNCATE"}};
+    String uuidTransformError =
+        "Partition transform %s is not supported on UUID column '%s'. Only IDENTITY, "
+        + "BUCKET, and VOID transforms are supported on UUID columns.";
+    for (String[] transform : rejectedUuidTransforms) {
+      AnalysisError("CREATE TABLE tbl1 (i int, u uuid) PARTITIONED BY SPEC (" +
+          transform[0] + ") STORED AS ICEBERG" + tblProperties,
+          String.format(uuidTransformError, transform[1], "u"));
+      AnalysisError("ALTER TABLE functional_parquet.iceberg_uuid_test " +
+          "SET PARTITION SPEC (" + transform[0].replace("u)", "uuid_col)") + ")",
+          String.format(uuidTransformError, transform[1], "uuid_col"));
+    }
+    AnalyzesOk("ALTER TABLE functional_parquet.iceberg_uuid_test " +
+        "SET PARTITION SPEC (uuid_col)");
+    AnalyzesOk("ALTER TABLE functional_parquet.iceberg_uuid_test " +
+        "SET PARTITION SPEC (BUCKET(4, uuid_col))");
+    AnalyzesOk("ALTER TABLE functional_parquet.iceberg_uuid_test " +
+        "SET PARTITION SPEC (VOID(uuid_col))");
+    // Source column references are case-insensitive.
+    AnalyzesOk("CREATE TABLE tbl1 (i int, u uuid) PARTITIONED BY SPEC " +
+        "(U, BUCKET(4, U)) STORED AS ICEBERG" + tblProperties);
+    AnalysisError("CREATE TABLE tbl1 (i int, u uuid) PARTITIONED BY SPEC (YEAR(U)) " +
+        "STORED AS ICEBERG" + tblProperties,
+        String.format(uuidTransformError, "YEAR", "u"));
+    AnalyzesOk("ALTER TABLE functional_parquet.iceberg_uuid_test " +
+        "SET PARTITION SPEC (BUCKET(4, UUID_COL))");
+    AnalysisError("ALTER TABLE functional_parquet.iceberg_uuid_test " +
+        "SET PARTITION SPEC (TRUNCATE(4, UUID_COL))",
+        String.format(uuidTransformError, "TRUNCATE", "uuid_col"));
   }
 
   @Test
diff --git a/testdata/data/README b/testdata/data/README
index 464ed7560..ae315b165 100644
--- a/testdata/data/README
+++ b/testdata/data/README
@@ -1032,6 +1032,26 @@ Generated using the Iceberg Java API.
 UUID values are stored as Avro fixed(16) with logicalType=uuid.
 Used to verify Avro UUID reads are rejected until a later patch.
 
+iceberg_test/iceberg_uuid/iceberg_uuid_test_part:
+Iceberg V3 Parquet table for UUID partition transform tests.
+Generated using the Iceberg Java API (version 1.11.0).
+Columns: id (int), uuid_identity (uuid), uuid_bucket (uuid).
+Partition spec: identity(uuid_identity), bucket(4, uuid_bucket).
+Rows (uuid_identity, uuid_bucket, bucket):
+  1: 00000000-0000-0000-0000-000000000000 for both (bucket 0): minimum value
+  2: ffffffff-ffff-ffff-ffff-ffffffffffff for both (bucket 2): maximum value, negative
+     as a signed long
+  3: 12345678-1234-5678-1234-567812345678 for both (bucket 1): non-uniform bytes, so
+     byte-order mistakes are visible
+  4: 12345678-1234-5678-1234-567812345678, 80000000-0000-0000-0000-000000000000
+     (bucket 1): smallest negative signed value, in the same data file as row 3
+  5: NULL for both (null partition values)
+Bucket numbers are computed by Iceberg (Murmur3 over the 16 big-endian UUID bytes).
+All four data files are added in a single append, so one manifest holds every
+partition. Its uuid_identity summary, built with Iceberg's signed UUID comparator,
+spans the sign bit: lower ffffffff-ffff-ffff-ffff-ffffffffffff, upper
+12345678-1234-5678-1234-567812345678.
+
 iceberg_v2_positional_delete_all_rows:
 iceberg_v2_positional_delete_all_rows_orc:
 Created similarly to iceberg_v2_no_deletes.
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=00000000-0000-0000-0000-000000000000/uuid_bucket_bucket=0/00000-0-16e8a22e-d632-4420-ac01-8b1e90eb07a0-1-00001.parquet b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=00000000-0000-0000-0000-000000000000/uuid_bucket_bucket=0/00000-0-16e8a22e-d632-4420-ac01-8b1e90eb07a0-1-00001.parquet
new file mode 100644
index 000000000..074f96c43
Binary files /dev/null and b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=00000000-0000-0000-0000-000000000000/uuid_bucket_bucket=0/00000-0-16e8a22e-d632-4420-ac01-8b1e90eb07a0-1-00001.parquet differ
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=12345678-1234-5678-1234-567812345678/uuid_bucket_bucket=1/00000-0-9f76a792-853c-495b-a239-86f69d1d8e38-1-00001.parquet b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=12345678-1234-5678-1234-567812345678/uuid_bucket_bucket=1/00000-0-9f76a792-853c-495b-a239-86f69d1d8e38-1-00001.parquet
new file mode 100644
index 000000000..64cbf316b
Binary files /dev/null and b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=12345678-1234-5678-1234-567812345678/uuid_bucket_bucket=1/00000-0-9f76a792-853c-495b-a239-86f69d1d8e38-1-00001.parquet differ
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=ffffffff-ffff-ffff-ffff-ffffffffffff/uuid_bucket_bucket=2/00000-0-46c79c37-060a-4877-acd7-40c9e162917c-1-00001.parquet b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=ffffffff-ffff-ffff-ffff-ffffffffffff/uuid_bucket_bucket=2/00000-0-46c79c37-060a-4877-acd7-40c9e162917c-1-00001.parquet
new file mode 100644
index 000000000..73fa36f3f
Binary files /dev/null and b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=ffffffff-ffff-ffff-ffff-ffffffffffff/uuid_bucket_bucket=2/00000-0-46c79c37-060a-4877-acd7-40c9e162917c-1-00001.parquet differ
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=null/uuid_bucket_bucket=null/00000-0-0803bc42-347b-4eea-98c7-ad80a9c97275-1-00001.parquet b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=null/uuid_bucket_bucket=null/00000-0-0803bc42-347b-4eea-98c7-ad80a9c97275-1-00001.parquet
new file mode 100644
index 000000000..0bf089290
Binary files /dev/null and b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/data/uuid_identity=null/uuid_bucket_bucket=null/00000-0-0803bc42-347b-4eea-98c7-ad80a9c97275-1-00001.parquet differ
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/4757457c-ce31-46a5-aa84-18521e2a2903-m0.avro b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/4757457c-ce31-46a5-aa84-18521e2a2903-m0.avro
new file mode 100644
index 000000000..d052fefa8
Binary files /dev/null and b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/4757457c-ce31-46a5-aa84-18521e2a2903-m0.avro differ
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/snap-2882928824223409296-1-4757457c-ce31-46a5-aa84-18521e2a2903.avro b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/snap-2882928824223409296-1-4757457c-ce31-46a5-aa84-18521e2a2903.avro
new file mode 100644
index 000000000..49da969ba
Binary files /dev/null and b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/snap-2882928824223409296-1-4757457c-ce31-46a5-aa84-18521e2a2903.avro differ
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/v1.metadata.json b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/v1.metadata.json
new file mode 100644
index 000000000..8711f6e53
--- /dev/null
+++ b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/v1.metadata.json
@@ -0,0 +1 @@
+{"format-version":3,"table-uuid":"7402ae4b-9e90-45c6-a326-cd8155112f45","location":"/test-warehouse/iceberg_uuid_test_part","last-sequence-number":0,"last-updated-ms":1791015142436,"last-column-id":3,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":false,"type":"int"},{"id":2,"name":"uuid_identity","required":false,"type":"uuid"},{"id":3,"name":"uuid_bucket","required":false,"type":"uuid"}]}],"default-spec-id":0,"partition-specs":[{ [...]
\ No newline at end of file
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/v2.metadata.json b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/v2.metadata.json
new file mode 100644
index 000000000..b6d5b0ba1
--- /dev/null
+++ b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/v2.metadata.json
@@ -0,0 +1 @@
+{"format-version":3,"table-uuid":"7402ae4b-9e90-45c6-a326-cd8155112f45","location":"/test-warehouse/iceberg_uuid_test_part","last-sequence-number":1,"last-updated-ms":1791015143483,"last-column-id":3,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":false,"type":"int"},{"id":2,"name":"uuid_identity","required":false,"type":"uuid"},{"id":3,"name":"uuid_bucket","required":false,"type":"uuid"}]}],"default-spec-id":0,"partition-specs":[{ [...]
\ No newline at end of file
diff --git a/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/version-hint.text b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/version-hint.text
new file mode 100644
index 000000000..d8263ee98
--- /dev/null
+++ b/testdata/data/iceberg_test/iceberg_uuid/iceberg_uuid_test_part/metadata/version-hint.text
@@ -0,0 +1 @@
+2
\ No newline at end of file
diff --git a/testdata/workloads/functional-query/queries/QueryTest/iceberg-trino-interop-uuid.test b/testdata/workloads/functional-query/queries/QueryTest/iceberg-trino-interop-uuid.test
index f4c2eebec..ff58e4806 100644
--- a/testdata/workloads/functional-query/queries/QueryTest/iceberg-trino-interop-uuid.test
+++ b/testdata/workloads/functional-query/queries/QueryTest/iceberg-trino-interop-uuid.test
@@ -272,3 +272,115 @@ SELECT id, uuid_col FROM ti_uuid_mixed_avro ORDER BY id
 ---- CATCH
 Reading UUID columns from AVRO format is not yet supported
 ====
+---- TRINO_QUERY
+# Partition evolution: start with identity partitioning on uuid_col.
+CREATE TABLE ti_uuid_part_evolve (id integer, uuid_col uuid, name varchar)
+WITH (format_version=3, format='PARQUET', partitioning = ARRAY['uuid_col']);
+INSERT INTO ti_uuid_part_evolve VALUES
+  (1, UUID '12345678-1234-5678-1234-567812345678', 'identity_alice'),
+  (2, UUID 'abcdefab-cdef-abcd-efab-cdefabcdefab', 'identity_bob');
+# Evolve to bucket(4, uuid_col).
+ALTER TABLE ti_uuid_part_evolve SET PROPERTIES partitioning = ARRAY['bucket(uuid_col, 4)'];
+INSERT INTO ti_uuid_part_evolve VALUES
+  (3, UUID 'ffffffff-ffff-ffff-ffff-ffffffffffff', 'bucket_max'),
+  (4, NULL, 'bucket_null');
+# Same UUID as row 2, written under the bucket spec.
+INSERT INTO ti_uuid_part_evolve VALUES
+  (5, UUID 'abcdefab-cdef-abcd-efab-cdefabcdefab', 'bucket_bob');
+====
+---- QUERY
+# Impala reads rows written under both the identity and bucket partition specs.
+INVALIDATE METADATA ti_uuid_part_evolve;
+SELECT id, uuid_col, name FROM ti_uuid_part_evolve ORDER BY id;
+---- RESULTS
+1,'12345678-1234-5678-1234-567812345678','identity_alice'
+2,'abcdefab-cdef-abcd-efab-cdefabcdefab','identity_bob'
+3,'ffffffff-ffff-ffff-ffff-ffffffffffff','bucket_max'
+4,'NULL','bucket_null'
+5,'abcdefab-cdef-abcd-efab-cdefabcdefab','bucket_bob'
+---- TYPES
+INT,UUID,STRING
+---- HS2_TYPES
+INT,STRING,STRING
+====
+---- QUERY
+# Both partition specs are visible after evolution. Rows 3 and 5 were written by separate
+# INSERTs into bucket 2.
+SHOW PARTITIONS ti_uuid_part_evolve
+---- RESULTS
+'{"uuid_col":"12345678-1234-5678-1234-567812345678"}',1,1
+'{"uuid_col":"abcdefab-cdef-abcd-efab-cdefabcdefab"}',1,1
+'{"uuid_col_bucket":"2"}',2,2
+'{"uuid_col_bucket":null}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+# Equality on the UUID source column matches rows from both partition specs.
+SELECT id FROM ti_uuid_part_evolve
+WHERE uuid_col = CAST('abcdefab-cdef-abcd-efab-cdefabcdefab' AS UUID)
+ORDER BY id
+---- RESULTS
+2
+5
+---- TYPES
+INT
+====
+---- QUERY
+# UUID value predicates are evaluated in the scan, not pushed to Iceberg.
+SELECT id FROM ti_uuid_part_evolve
+WHERE uuid_col > CAST('20000000-0000-0000-0000-000000000000' AS UUID)
+ORDER BY id
+---- RESULTS
+2
+3
+5
+---- TYPES
+INT
+====
+---- QUERY
+SELECT id FROM ti_uuid_part_evolve WHERE uuid_col IS NULL
+---- RESULTS
+4
+---- TYPES
+INT
+====
+---- TRINO_QUERY
+# Identity-partitioned table: one INSERT writes a single manifest whose partition summary
+# spans UUIDs on both sides of the signed-long sign bit.
+CREATE TABLE ti_uuid_identity_drop (id integer, uuid_col uuid, name varchar)
+WITH (format_version=3, format='PARQUET', partitioning = ARRAY['uuid_col']);
+INSERT INTO ti_uuid_identity_drop VALUES
+  (1, UUID '12345678-1234-5678-1234-567812345678', 'positive'),
+  (2, UUID '80000000-0000-0000-0000-000000000000', 'sign_bit'),
+  (3, UUID 'ffffffff-ffff-ffff-ffff-ffffffffffff', 'max');
+====
+---- QUERY
+# Impala reads the Trino-written identity partitions.
+INVALIDATE METADATA ti_uuid_identity_drop;
+SHOW PARTITIONS ti_uuid_identity_drop
+---- RESULTS
+'{"uuid_col":"12345678-1234-5678-1234-567812345678"}',1,1
+'{"uuid_col":"80000000-0000-0000-0000-000000000000"}',1,1
+'{"uuid_col":"ffffffff-ffff-ffff-ffff-ffffffffffff"}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+# Trino builds the manifest summary with Iceberg's signed UUID comparator, so manifest
+# pruning keeps the partition of ffffffff-..., which is negative as a signed long.
+ALTER TABLE ti_uuid_identity_drop
+DROP PARTITION (uuid_col = CAST('ffffffff-ffff-ffff-ffff-ffffffffffff' AS UUID))
+---- RESULTS
+'Dropped 1 partition(s)'
+---- TYPES
+STRING
+====
+---- QUERY
+SHOW PARTITIONS ti_uuid_identity_drop
+---- RESULTS
+'{"uuid_col":"12345678-1234-5678-1234-567812345678"}',1,1
+'{"uuid_col":"80000000-0000-0000-0000-000000000000"}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
diff --git a/testdata/workloads/functional-query/queries/QueryTest/iceberg-uuid-type.test b/testdata/workloads/functional-query/queries/QueryTest/iceberg-uuid-type.test
index f265f2c17..c4e909cea 100644
--- a/testdata/workloads/functional-query/queries/QueryTest/iceberg-uuid-type.test
+++ b/testdata/workloads/functional-query/queries/QueryTest/iceberg-uuid-type.test
@@ -743,3 +743,279 @@ COMPUTE STATS iceberg_uuid_test (id)
 ---- TYPES
 STRING
 ====
+---- QUERY
+# Table partitioned by identity(uuid_identity) and bucket(4, uuid_bucket).
+SELECT id, uuid_identity, uuid_bucket FROM iceberg_uuid_test_part ORDER BY id
+---- RESULTS
+1,'00000000-0000-0000-0000-000000000000','00000000-0000-0000-0000-000000000000'
+2,'ffffffff-ffff-ffff-ffff-ffffffffffff','ffffffff-ffff-ffff-ffff-ffffffffffff'
+3,'12345678-1234-5678-1234-567812345678','12345678-1234-5678-1234-567812345678'
+4,'12345678-1234-5678-1234-567812345678','80000000-0000-0000-0000-000000000000'
+5,'NULL','NULL'
+---- TYPES
+INT,UUID,UUID
+---- HS2_TYPES
+INT,STRING,STRING
+====
+---- QUERY
+# Identity partition values are the canonical UUID strings. Rows 3 and 4 share one data
+# file whose uuid_bucket values (12345678-... and 80000000-...) are on opposite sides of
+# the signed-long sign bit.
+SHOW PARTITIONS iceberg_uuid_test_part
+---- RESULTS
+'{"uuid_identity":"00000000-0000-0000-0000-000000000000","uuid_bucket_bucket":"0"}',1,1
+'{"uuid_identity":"12345678-1234-5678-1234-567812345678","uuid_bucket_bucket":"1"}',2,1
+'{"uuid_identity":"ffffffff-ffff-ffff-ffff-ffffffffffff","uuid_bucket_bucket":"2"}',1,1
+'{"uuid_identity":null,"uuid_bucket_bucket":null}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+SHOW CREATE TABLE iceberg_uuid_test_part
+---- RESULTS: VERIFY_IS_SUBSET
+row_regex: .*PARTITIONED BY SPEC.*uuid_identity.*BUCKET\(4. uuid_bucket\).*
+---- TYPES
+STRING
+====
+---- QUERY
+SELECT id FROM iceberg_uuid_test_part
+WHERE uuid_identity = CAST('12345678-1234-5678-1234-567812345678' AS UUID)
+ORDER BY id
+---- RESULTS
+3
+4
+---- TYPES
+INT
+====
+---- QUERY
+# Range predicates use unsigned UUID order: ffffffff-... is negative as a signed long.
+SELECT id FROM iceberg_uuid_test_part
+WHERE uuid_identity > CAST('20000000-0000-0000-0000-000000000000' AS UUID)
+ORDER BY id
+---- RESULTS
+2
+---- TYPES
+INT
+====
+---- QUERY
+SELECT id FROM iceberg_uuid_test_part WHERE uuid_identity IS NULL ORDER BY id
+---- RESULTS
+5
+---- TYPES
+INT
+====
+---- QUERY
+# The next three predicates hit the data file shared by rows 3 and 4. Its unsigned
+# min/max bounds are misordered under Iceberg's signed UUID comparison, so Iceberg would
+# skip the file if these predicates were pushed down.
+SELECT id FROM iceberg_uuid_test_part
+WHERE uuid_bucket = CAST('12345678-1234-5678-1234-567812345678' AS UUID)
+---- RESULTS
+3
+---- TYPES
+INT
+====
+---- QUERY
+SELECT id FROM iceberg_uuid_test_part
+WHERE uuid_bucket IN (CAST('12345678-1234-5678-1234-567812345678' AS UUID),
+                      CAST('80000000-0000-0000-0000-000000000000' AS UUID))
+ORDER BY id
+---- RESULTS
+3
+4
+---- TYPES
+INT
+====
+---- QUERY
+# ffffffff-... and 80000000-... are negative as signed longs, so a signed comparison
+# would return no rows.
+SELECT id FROM iceberg_uuid_test_part
+WHERE uuid_bucket > CAST('20000000-0000-0000-0000-000000000000' AS UUID)
+ORDER BY id
+---- RESULTS
+2
+4
+---- TYPES
+INT
+====
+---- QUERY
+# UUID value predicates stay in the scan and Iceberg prunes no data files.
+EXPLAIN SELECT id FROM iceberg_uuid_test_part
+WHERE uuid_bucket = CAST('12345678-1234-5678-1234-567812345678' AS UUID);
+---- RESULTS: VERIFY_IS_SUBSET
+row_regex: .*HDFS partitions=4/4 files=4 .*
+row_regex: .*   predicates: uuid_bucket = ''12345678-1234-5678-1234-567812345678''.*
+---- TYPES
+STRING
+====
+---- QUERY
+# IS NULL does not compare UUID values, so it is pushed to Iceberg and prunes partitions.
+EXPLAIN SELECT id FROM iceberg_uuid_test_part WHERE uuid_identity IS NULL;
+---- RESULTS: VERIFY_IS_SUBSET
+row_regex: .*HDFS partitions=1/4 files=1 .*
+row_regex: .*skipped Iceberg predicates: uuid_identity IS NULL.*
+---- TYPES
+STRING
+====
+---- QUERY
+# SHOW PARTITIONS WHERE with an identity UUID = filter. All partitions are in one
+# manifest whose signed uuid_identity summary is [ffffffff-..., 12345678-...], so
+# Iceberg must not prune it for values on either side of the sign bit.
+SHOW PARTITIONS iceberg_uuid_test_part
+WHERE uuid_identity = CAST('00000000-0000-0000-0000-000000000000' AS UUID)
+---- RESULTS
+'{"uuid_identity":"00000000-0000-0000-0000-000000000000","uuid_bucket_bucket":"0"}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+SHOW PARTITIONS iceberg_uuid_test_part
+WHERE uuid_identity = CAST('ffffffff-ffff-ffff-ffff-ffffffffffff' AS UUID)
+---- RESULTS
+'{"uuid_identity":"ffffffff-ffff-ffff-ffff-ffffffffffff","uuid_bucket_bucket":"2"}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+SHOW PARTITIONS iceberg_uuid_test_part
+WHERE uuid_identity IN (CAST('00000000-0000-0000-0000-000000000000' AS UUID),
+                        CAST('12345678-1234-5678-1234-567812345678' AS UUID))
+---- RESULTS
+'{"uuid_identity":"00000000-0000-0000-0000-000000000000","uuid_bucket_bucket":"0"}',1,1
+'{"uuid_identity":"12345678-1234-5678-1234-567812345678","uuid_bucket_bucket":"1"}',2,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+# Iceberg matches the NULL partition for != and NOT IN.
+SHOW PARTITIONS iceberg_uuid_test_part
+WHERE uuid_identity != CAST('00000000-0000-0000-0000-000000000000' AS UUID)
+---- RESULTS
+'{"uuid_identity":"12345678-1234-5678-1234-567812345678","uuid_bucket_bucket":"1"}',2,1
+'{"uuid_identity":"ffffffff-ffff-ffff-ffff-ffffffffffff","uuid_bucket_bucket":"2"}',1,1
+'{"uuid_identity":null,"uuid_bucket_bucket":null}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+SHOW PARTITIONS iceberg_uuid_test_part
+WHERE uuid_identity NOT IN (CAST('00000000-0000-0000-0000-000000000000' AS UUID),
+                            CAST('12345678-1234-5678-1234-567812345678' AS UUID))
+---- RESULTS
+'{"uuid_identity":"ffffffff-ffff-ffff-ffff-ffffffffffff","uuid_bucket_bucket":"2"}',1,1
+'{"uuid_identity":null,"uuid_bucket_bucket":null}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+SHOW PARTITIONS iceberg_uuid_test_part
+WHERE uuid_identity > CAST('20000000-0000-0000-0000-000000000000' AS UUID)
+---- CATCH
+UUID values can only be used with =, !=, IN or NOT IN on columns that are identity partitioned in every partition spec
+====
+---- QUERY
+# SHOW FILES PARTITION with an identity UUID = filter returns one data file.
+SHOW FILES IN iceberg_uuid_test_part
+PARTITION (uuid_identity = CAST('00000000-0000-0000-0000-000000000000' AS UUID))
+---- RESULTS
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=00000000-0000-0000-0000-000000000000/.*\.parquet','.*','','$ERASURECODE_POLICY'
+---- TYPES
+STRING,STRING,STRING,STRING
+====
+---- QUERY
+SHOW FILES IN iceberg_uuid_test_part
+PARTITION (uuid_identity IN (CAST('00000000-0000-0000-0000-000000000000' AS UUID),
+                             CAST('ffffffff-ffff-ffff-ffff-ffffffffffff' AS UUID)))
+---- RESULTS
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=00000000-0000-0000-0000-000000000000/.*\.parquet','.*','','$ERASURECODE_POLICY'
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=ffffffff-ffff-ffff-ffff-ffffffffffff/.*\.parquet','.*','','$ERASURECODE_POLICY'
+---- TYPES
+STRING,STRING,STRING,STRING
+====
+---- QUERY
+SHOW FILES IN iceberg_uuid_test_part
+PARTITION (uuid_identity != CAST('00000000-0000-0000-0000-000000000000' AS UUID))
+---- RESULTS
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=12345678-1234-5678-1234-567812345678/.*\.parquet','.*','','$ERASURECODE_POLICY'
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=ffffffff-ffff-ffff-ffff-ffffffffffff/.*\.parquet','.*','','$ERASURECODE_POLICY'
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=null/.*\.parquet','.*','','$ERASURECODE_POLICY'
+---- TYPES
+STRING,STRING,STRING,STRING
+====
+---- QUERY
+SHOW FILES IN iceberg_uuid_test_part
+PARTITION (uuid_identity NOT IN (CAST('00000000-0000-0000-0000-000000000000' AS UUID),
+                                 CAST('12345678-1234-5678-1234-567812345678' AS UUID)))
+---- RESULTS
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=ffffffff-ffff-ffff-ffff-ffffffffffff/.*\.parquet','.*','','$ERASURECODE_POLICY'
+row_regex:'$NAMENODE/test-warehouse/$DATABASE.db/iceberg_uuid_test_part/data/uuid_identity=null/.*\.parquet','.*','','$ERASURECODE_POLICY'
+---- TYPES
+STRING,STRING,STRING,STRING
+====
+---- QUERY
+SHOW FILES IN iceberg_uuid_test_part
+PARTITION (uuid_identity > CAST('20000000-0000-0000-0000-000000000000' AS UUID))
+---- CATCH
+UUID values can only be used with =, !=, IN or NOT IN on columns that are identity partitioned in every partition spec
+====
+---- QUERY
+# A bucket partition filter compares INT bucket numbers, so it converts to an Iceberg
+# expression. Bucket 3 has no data files.
+ALTER TABLE iceberg_uuid_test_part DROP PARTITION (bucket(4, uuid_bucket) = 3)
+---- CATCH
+No matching partition(s) found
+====
+---- QUERY
+# An identity UUID partition filter with no matching partition.
+ALTER TABLE iceberg_uuid_test_part
+DROP PARTITION (uuid_identity = CAST('22222222-2222-2222-2222-222222222222' AS UUID))
+---- CATCH
+No matching partition(s) found
+====
+---- QUERY
+# Range predicates on UUID identity partitions are rejected.
+ALTER TABLE iceberg_uuid_test_part
+DROP PARTITION (uuid_identity > CAST('20000000-0000-0000-0000-000000000000' AS UUID))
+---- CATCH
+UUID values can only be used with =, !=, IN or NOT IN on columns that are identity partitioned in every partition spec
+====
+---- QUERY
+# IN with two UUID literals drops both matching identity partitions. This runs first,
+# while the single manifest's signed uuid_identity summary is [ffffffff-..., 12345678-...].
+ALTER TABLE iceberg_uuid_test_part
+DROP PARTITION (uuid_identity IN (CAST('ffffffff-ffff-ffff-ffff-ffffffffffff' AS UUID),
+                                  CAST('00000000-0000-0000-0000-000000000000' AS UUID)))
+====
+---- QUERY
+SHOW PARTITIONS iceberg_uuid_test_part
+---- RESULTS
+'{"uuid_identity":"12345678-1234-5678-1234-567812345678","uuid_bucket_bucket":"1"}',2,1
+'{"uuid_identity":null,"uuid_bucket_bucket":null}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+# Drop the bucket=1 partition (uuid_identity=12345678-...).
+ALTER TABLE iceberg_uuid_test_part DROP PARTITION (bucket(4, uuid_bucket) = 1)
+====
+---- QUERY
+SHOW PARTITIONS iceberg_uuid_test_part
+---- RESULTS
+'{"uuid_identity":null,"uuid_bucket_bucket":null}',1,1
+---- TYPES
+STRING,BIGINT,BIGINT
+====
+---- QUERY
+CREATE TABLE uuid_spec_evolution (i INT, u UUID)
+PARTITIONED BY SPEC (BUCKET(4, u)) STORED AS ICEBERG
+====
+---- QUERY
+ALTER TABLE uuid_spec_evolution SET PARTITION SPEC (u)
+====
+---- QUERY
+# u was bucket-partitioned in an older spec, so UUID values are rejected.
+ALTER TABLE uuid_spec_evolution
+DROP PARTITION (u = CAST('12345678-1234-5678-1234-567812345678' AS UUID))
+---- CATCH
+UUID values can only be used with =, !=, IN or NOT IN on columns that are identity partitioned in every partition spec
+====
diff --git a/tests/query_test/test_iceberg.py b/tests/query_test/test_iceberg.py
index b7d1c1be4..21ec09421 100644
--- a/tests/query_test/test_iceberg.py
+++ b/tests/query_test/test_iceberg.py
@@ -84,9 +84,17 @@ class TestIcebergTable(IcebergTestSuite):
   def test_iceberg_binary_type(self, vector, unique_database):
     self.run_test_case('QueryTest/iceberg-binary-type', vector, use_db=unique_database)
 
-  def test_iceberg_uuid_type(self, vector):
-    self.run_test_case('QueryTest/iceberg-uuid-type', vector,
-        use_db="functional_parquet")
+  def test_iceberg_uuid_type(self, vector, unique_database):
+    uuid_fixture_dir = "${IMPALA_HOME}/testdata/data/iceberg_test/iceberg_uuid"
+    create_iceberg_table_from_directory(self.client, unique_database,
+        "iceberg_uuid_test", "parquet", table_location=uuid_fixture_dir)
+    create_iceberg_table_from_directory(self.client, unique_database,
+        "iceberg_uuid_test_orc", "orc", table_location=uuid_fixture_dir)
+    create_iceberg_table_from_directory(self.client, unique_database,
+        "iceberg_uuid_test_avro", "avro", table_location=uuid_fixture_dir)
+    create_iceberg_table_from_directory(self.client, unique_database,
+        "iceberg_uuid_test_part", "parquet", table_location=uuid_fixture_dir)
+    self.run_test_case('QueryTest/iceberg-uuid-type', vector, use_db=unique_database)
 
   def test_utf8_string_validation(self, unique_database):
     """IMPALA-12675: Iceberg always annotates STRING as UTF-8, so writing non-UTF-8 bytes