diff --git a/api/src/main/java/org/apache/iceberg/transforms/Identity.java b/api/src/main/java/org/apache/iceberg/transforms/Identity.java index f52e63633578..99489f939f0f 100644 --- a/api/src/main/java/org/apache/iceberg/transforms/Identity.java +++ b/api/src/main/java/org/apache/iceberg/transforms/Identity.java @@ -21,6 +21,7 @@ import java.io.ObjectStreamException; import java.util.Set; import org.apache.iceberg.expressions.BoundPredicate; +import org.apache.iceberg.expressions.BoundReference; import org.apache.iceberg.expressions.Expressions; import org.apache.iceberg.expressions.UnboundPredicate; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; @@ -146,6 +147,10 @@ public UnboundPredicate project(String name, BoundPredicate predicate) { @Override public UnboundPredicate projectStrict(String name, BoundPredicate predicate) { + if (!(predicate.term() instanceof BoundReference)) { + return null; + } + if (predicate.isUnaryPredicate()) { return Expressions.predicate(predicate.op(), name); } else if (predicate.isLiteralPredicate()) { diff --git a/api/src/test/java/org/apache/iceberg/transforms/TestProjection.java b/api/src/test/java/org/apache/iceberg/transforms/TestProjection.java index f6c699f7b616..2ad0cee80aed 100644 --- a/api/src/test/java/org/apache/iceberg/transforms/TestProjection.java +++ b/api/src/test/java/org/apache/iceberg/transforms/TestProjection.java @@ -394,4 +394,30 @@ public void testProjectionNames() { Projections.inclusive(partitionSpec).project(equal(truncate("string", 10), "abc")); assertThat(predicate.ref().name()).isEqualTo("string_trunc"); } + + @Test + public void testIdentityProjectionWithTransformPredicate() { + // Regression test for https://github.com/apache/iceberg/issues/15502 + // Identity-partitioned timestamptz field filtered with hours() should not throw + // ValidationException when projecting the predicate. + Schema schema = + new Schema( + required(1, "id", Types.LongType.get()), + required(2, "ts", Types.TimestampType.withZone())); + + PartitionSpec spec = PartitionSpec.builderFor(schema).identity("ts").build(); + + // hours(ts) = 490674 produces an integer literal that cannot bind to timestamptz. + // Without the fix, projectStrict() attempts to create an UnboundPredicate with the + // integer literal for a timestamptz field, which fails during binding. + Expression hourFilter = equal(hour("ts"), 490674); + + // Inclusive projection: cannot project → falls back to alwaysTrue + Expression projected = Projections.inclusive(spec).project(hourFilter); + assertThat(projected).isEqualTo(Expressions.alwaysTrue()); + + // Strict projection: cannot project → falls back to alwaysFalse + Expression strictProjected = Projections.strict(spec).project(hourFilter); + assertThat(strictProjected).isEqualTo(Expressions.alwaysFalse()); + } }