Skip to content

Commit 9b0dd32

Browse files
authored
API: Guard against null in IN/NOT_IN predicates (#17014) (#17183)
(cherry picked from commit 187da3e)
1 parent 62b4d3d commit 9b0dd32

6 files changed

Lines changed: 90 additions & 5 deletions

File tree

api/src/main/java/org/apache/iceberg/expressions/BoundSetPredicate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ public Set<T> literalSet() {
5858
public boolean test(T value) {
5959
switch (op()) {
6060
case IN:
61-
return literalSet.contains(value);
61+
return value != null && literalSet.contains(value);
6262
case NOT_IN:
63-
return !literalSet.contains(value);
63+
return value == null || !literalSet.contains(value);
6464
default:
6565
throw new IllegalStateException("Invalid operation for BoundSetPredicate: " + op());
6666
}

api/src/main/java/org/apache/iceberg/expressions/Evaluator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ public <T> Boolean notEq(Bound<T> valueExpr, Literal<T> lit) {
138138

139139
@Override
140140
public <T> Boolean in(Bound<T> valueExpr, Set<T> literalSet) {
141-
return literalSet.contains(valueExpr.eval(struct));
141+
T value = valueExpr.eval(struct);
142+
return value != null && literalSet.contains(value);
142143
}
143144

144145
@Override

api/src/main/java/org/apache/iceberg/expressions/ResidualEvaluator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,14 @@ public <T> Expression notEq(BoundReference<T> ref, Literal<T> lit) {
200200

201201
@Override
202202
public <T> Expression in(BoundReference<T> ref, Set<T> literalSet) {
203-
return literalSet.contains(ref.eval(struct)) ? alwaysTrue() : alwaysFalse();
203+
T value = ref.eval(struct);
204+
return value != null && literalSet.contains(value) ? alwaysTrue() : alwaysFalse();
204205
}
205206

206207
@Override
207208
public <T> Expression notIn(BoundReference<T> ref, Set<T> literalSet) {
208-
return literalSet.contains(ref.eval(struct)) ? alwaysFalse() : alwaysTrue();
209+
T value = ref.eval(struct);
210+
return value == null || !literalSet.contains(value) ? alwaysTrue() : alwaysFalse();
209211
}
210212

211213
@Override

api/src/test/java/org/apache/iceberg/expressions/TestEvaluator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,12 @@ public void testIn() {
674674
assertThat(charSeqEvaluator.eval(TestHelpers.Row.of(new Utf8("abcd"))))
675675
.as("utf8(abcd) in [string(abc), string(abd)] => false")
676676
.isFalse();
677+
678+
StructType nullableStringStruct = StructType.of(optional(35, "s", Types.StringType.get()));
679+
Evaluator nullStringEvaluator = new Evaluator(nullableStringStruct, in("s", "abc", "abd"));
680+
assertThat(nullStringEvaluator.eval(TestHelpers.Row.of((String) null)))
681+
.as("null in [abc, abd] => false")
682+
.isFalse();
677683
}
678684

679685
@Test
@@ -776,6 +782,12 @@ public void testNotIn() {
776782
assertThat(charSeqEvaluator.eval(TestHelpers.Row.of(new Utf8("abcd"))))
777783
.as("utf8(abcd) not in [string(abc), string(abd)] => true")
778784
.isTrue();
785+
786+
StructType nullableStringStruct = StructType.of(optional(35, "s", Types.StringType.get()));
787+
Evaluator nullStringEvaluator = new Evaluator(nullableStringStruct, notIn("s", "abc", "abd"));
788+
assertThat(nullStringEvaluator.eval(TestHelpers.Row.of((String) null)))
789+
.as("null not in [abc, abd] => true")
790+
.isTrue();
779791
}
780792

781793
@Test

api/src/test/java/org/apache/iceberg/expressions/TestPredicateBinding.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,30 @@ public void testInPredicateBindingConversionToExpression() {
528528
.isEqualTo(Expressions.alwaysFalse());
529529
}
530530

531+
@Test
532+
public void testInPredicateTestWithNull() {
533+
StructType struct = StructType.of(optional(36, "s", Types.StringType.get()));
534+
535+
Expression expr = Expressions.in("s", "abc", "abd").bind(struct);
536+
BoundSetPredicate<CharSequence> bound = assertAndUnwrapBoundSet(expr);
537+
538+
assertThat(bound.test((CharSequence) null)).as("null in [abc, abd] => false").isFalse();
539+
assertThat(bound.test("abc")).as("abc in [abc, abd] => true").isTrue();
540+
assertThat(bound.test("xyz")).as("xyz in [abc, abd] => false").isFalse();
541+
}
542+
543+
@Test
544+
public void testNotInPredicateTestWithNull() {
545+
StructType struct = StructType.of(optional(37, "s", Types.StringType.get()));
546+
547+
Expression expr = Expressions.notIn("s", "abc", "abd").bind(struct);
548+
BoundSetPredicate<CharSequence> bound = assertAndUnwrapBoundSet(expr);
549+
550+
assertThat(bound.test((CharSequence) null)).as("null not in [abc, abd] => true").isTrue();
551+
assertThat(bound.test("abc")).as("abc not in [abc, abd] => false").isFalse();
552+
assertThat(bound.test("xyz")).as("xyz not in [abc, abd] => true").isTrue();
553+
}
554+
531555
@Test
532556
public void testNotInPredicateBinding() {
533557
StructType struct =

api/src/test/java/org/apache/iceberg/transforms/TestResiduals.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,29 @@ public void testIn() {
193193

194194
residual = resEval.residualFor(Row.of(20180815));
195195
assertThat(residual).isEqualTo(alwaysFalse());
196+
197+
residual = resEval.residualFor(Row.of((Object) null));
198+
assertThat(residual)
199+
.as("null in [20170815, 20170816, 20170817] => false")
200+
.isEqualTo(alwaysFalse());
201+
}
202+
203+
@Test
204+
public void testInWithNullStringPartition() {
205+
Schema schema = new Schema(Types.NestedField.optional(50, "category", Types.StringType.get()));
206+
207+
PartitionSpec spec = PartitionSpec.builderFor(schema).identity("category").build();
208+
209+
ResidualEvaluator resEval = ResidualEvaluator.of(spec, in("category", "a", "b", "c"), true);
210+
211+
Expression residual = resEval.residualFor(Row.of("a"));
212+
assertThat(residual).isEqualTo(alwaysTrue());
213+
214+
residual = resEval.residualFor(Row.of("d"));
215+
assertThat(residual).isEqualTo(alwaysFalse());
216+
217+
residual = resEval.residualFor(Row.of((Object) null));
218+
assertThat(residual).as("null in [a, b, c] => false").isEqualTo(alwaysFalse());
196219
}
197220

198221
@Test
@@ -241,6 +264,29 @@ public void testNotIn() {
241264

242265
residual = resEval.residualFor(Row.of(20170815));
243266
assertThat(residual).isEqualTo(alwaysFalse());
267+
268+
residual = resEval.residualFor(Row.of((Object) null));
269+
assertThat(residual)
270+
.as("null not in [20170815, 20170816, 20170817] => true")
271+
.isEqualTo(alwaysTrue());
272+
}
273+
274+
@Test
275+
public void testNotInWithNullStringPartition() {
276+
Schema schema = new Schema(Types.NestedField.optional(50, "category", Types.StringType.get()));
277+
278+
PartitionSpec spec = PartitionSpec.builderFor(schema).identity("category").build();
279+
280+
ResidualEvaluator resEval = ResidualEvaluator.of(spec, notIn("category", "a", "b", "c"), true);
281+
282+
Expression residual = resEval.residualFor(Row.of("d"));
283+
assertThat(residual).isEqualTo(alwaysTrue());
284+
285+
residual = resEval.residualFor(Row.of("a"));
286+
assertThat(residual).isEqualTo(alwaysFalse());
287+
288+
residual = resEval.residualFor(Row.of((Object) null));
289+
assertThat(residual).as("null not in [a, b, c] => true").isEqualTo(alwaysTrue());
244290
}
245291

246292
@Test

0 commit comments

Comments
 (0)