diff --git a/api/src/main/java/org/apache/iceberg/expressions/ResidualEvaluator.java b/api/src/main/java/org/apache/iceberg/expressions/ResidualEvaluator.java index 3b60a75e41a7..b03675fa18a2 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/ResidualEvaluator.java +++ b/api/src/main/java/org/apache/iceberg/expressions/ResidualEvaluator.java @@ -212,16 +212,16 @@ public Expression notIn(BoundReference ref, Set literalSet) { @Override public Expression startsWith(BoundReference ref, Literal lit) { - return ((String) ref.eval(struct)).startsWith((String) lit.value()) - ? alwaysTrue() - : alwaysFalse(); + String value = (String) ref.eval(struct); + return value != null && value.startsWith((String) lit.value()) ? alwaysTrue() : alwaysFalse(); } @Override public Expression notStartsWith(BoundReference ref, Literal lit) { - return ((String) ref.eval(struct)).startsWith((String) lit.value()) - ? alwaysFalse() - : alwaysTrue(); + String value = (String) ref.eval(struct); + return value == null || !value.startsWith((String) lit.value()) + ? alwaysTrue() + : alwaysFalse(); } @Override diff --git a/api/src/test/java/org/apache/iceberg/transforms/TestResiduals.java b/api/src/test/java/org/apache/iceberg/transforms/TestResiduals.java index 61c662bbfbf9..b1e91428e56a 100644 --- a/api/src/test/java/org/apache/iceberg/transforms/TestResiduals.java +++ b/api/src/test/java/org/apache/iceberg/transforms/TestResiduals.java @@ -289,6 +289,44 @@ public void testNotInWithNullStringPartition() { assertThat(residual).as("null not in [a, b, c] => true").isEqualTo(alwaysTrue()); } + @Test + public void testStartsWithWithNullStringPartition() { + Schema schema = new Schema(Types.NestedField.optional(50, "category", Types.StringType.get())); + + PartitionSpec spec = PartitionSpec.builderFor(schema).identity("category").build(); + + ResidualEvaluator resEval = + ResidualEvaluator.of(spec, Expressions.startsWith("category", "a"), true); + + Expression residual = resEval.residualFor(Row.of("abc")); + assertThat(residual).isEqualTo(alwaysTrue()); + + residual = resEval.residualFor(Row.of("bcd")); + assertThat(residual).isEqualTo(alwaysFalse()); + + residual = resEval.residualFor(Row.of((Object) null)); + assertThat(residual).as("null startsWith a => false").isEqualTo(alwaysFalse()); + } + + @Test + public void testNotStartsWithWithNullStringPartition() { + Schema schema = new Schema(Types.NestedField.optional(50, "category", Types.StringType.get())); + + PartitionSpec spec = PartitionSpec.builderFor(schema).identity("category").build(); + + ResidualEvaluator resEval = + ResidualEvaluator.of(spec, Expressions.notStartsWith("category", "a"), true); + + Expression residual = resEval.residualFor(Row.of("bcd")); + assertThat(residual).isEqualTo(alwaysTrue()); + + residual = resEval.residualFor(Row.of("abc")); + assertThat(residual).isEqualTo(alwaysFalse()); + + residual = resEval.residualFor(Row.of((Object) null)); + assertThat(residual).as("null notStartsWith a => true").isEqualTo(alwaysTrue()); + } + @Test public void testIsNaN() { Schema schema =