diff --git a/api/src/main/java/com/netflix/iceberg/expressions/Evaluator.java b/api/src/main/java/com/netflix/iceberg/expressions/Evaluator.java index 92fe78f83..1a854e8f6 100644 --- a/api/src/main/java/com/netflix/iceberg/expressions/Evaluator.java +++ b/api/src/main/java/com/netflix/iceberg/expressions/Evaluator.java @@ -109,6 +109,11 @@ public Boolean gt(BoundReference ref, Literal lit) { return cmp.compare(ref.get(struct), lit.value()) > 0; } + @Override + public Boolean startsWith(BoundReference ref, Literal lit) { + return ref.get(struct).startsWith(lit.value()); + } + @Override public Boolean gtEq(BoundReference ref, Literal lit) { Comparator cmp = lit.comparator(); diff --git a/api/src/main/java/com/netflix/iceberg/expressions/Expression.java b/api/src/main/java/com/netflix/iceberg/expressions/Expression.java index 129a36ac8..e1e43647e 100644 --- a/api/src/main/java/com/netflix/iceberg/expressions/Expression.java +++ b/api/src/main/java/com/netflix/iceberg/expressions/Expression.java @@ -37,7 +37,8 @@ enum Operation { NOT_IN, NOT, AND, - OR; + OR, + STARTS_WITH; /** * @return the operation used when this is negated diff --git a/api/src/main/java/com/netflix/iceberg/expressions/ExpressionVisitors.java b/api/src/main/java/com/netflix/iceberg/expressions/ExpressionVisitors.java index 5f549622c..9cf02d637 100644 --- a/api/src/main/java/com/netflix/iceberg/expressions/ExpressionVisitors.java +++ b/api/src/main/java/com/netflix/iceberg/expressions/ExpressionVisitors.java @@ -91,6 +91,10 @@ public R notIn(BoundReference ref, Literal lit) { return null; } + public R startsWith(BoundReference ref, Literal lit) { + return null; + } + public R predicate(BoundPredicate pred) { switch (pred.op()) { case IS_NULL: @@ -113,6 +117,11 @@ public R predicate(BoundPredicate pred) { return in(pred.ref(), pred.literal()); case NOT_IN: return notIn(pred.ref(), pred.literal()); + case STARTS_WITH: +// due to the fact that startWith function in Expressions accepts only string types casting is a must here. +// in Unbound#bind function we added a check for that + + return startsWith((BoundReference) pred.ref(), (Literal) pred.literal()); default: throw new UnsupportedOperationException( "Unknown operation for predicate: " + pred.op()); diff --git a/api/src/main/java/com/netflix/iceberg/expressions/Expressions.java b/api/src/main/java/com/netflix/iceberg/expressions/Expressions.java index b7d98c537..9a28dd2ff 100644 --- a/api/src/main/java/com/netflix/iceberg/expressions/Expressions.java +++ b/api/src/main/java/com/netflix/iceberg/expressions/Expressions.java @@ -96,6 +96,10 @@ public static UnboundPredicate notEqual(String name, T value) { return new UnboundPredicate<>(Expression.Operation.NOT_EQ, ref(name), value); } + public static UnboundPredicate startsWith(String name, T value) { + return new UnboundPredicate<>(Operation.STARTS_WITH, ref(name), value); + } + public static UnboundPredicate predicate(Operation op, String name, T value) { Preconditions.checkArgument(op != Operation.IS_NULL && op != Operation.NOT_NULL, "Cannot create %s predicate inclusive a value", op); diff --git a/api/src/main/java/com/netflix/iceberg/expressions/Predicate.java b/api/src/main/java/com/netflix/iceberg/expressions/Predicate.java index 647b08bbd..18a66cd94 100644 --- a/api/src/main/java/com/netflix/iceberg/expressions/Predicate.java +++ b/api/src/main/java/com/netflix/iceberg/expressions/Predicate.java @@ -59,6 +59,8 @@ public String toString() { return String.valueOf(ref()) + " == " + literal(); case NOT_EQ: return String.valueOf(ref()) + " != " + literal(); + case STARTS_WITH: + return "startsWith(" + ref() + "," + literal() + ")"; // case IN: // break; // case NOT_IN: diff --git a/api/src/main/java/com/netflix/iceberg/expressions/UnboundPredicate.java b/api/src/main/java/com/netflix/iceberg/expressions/UnboundPredicate.java index 3a8db7529..90da1c5dc 100644 --- a/api/src/main/java/com/netflix/iceberg/expressions/UnboundPredicate.java +++ b/api/src/main/java/com/netflix/iceberg/expressions/UnboundPredicate.java @@ -21,6 +21,7 @@ import static com.netflix.iceberg.expressions.Expression.Operation.IS_NULL; import static com.netflix.iceberg.expressions.Expression.Operation.NOT_NULL; +import static com.netflix.iceberg.expressions.Expression.Operation.STARTS_WITH; public class UnboundPredicate extends Predicate { @@ -63,6 +64,10 @@ public Expression bind(Types.StructType struct) { } } + if (op() == STARTS_WITH && field.type() != Types.StringType.get()) { + throw new ValidationException("Operation startsWith accepts only strings"); + } + Literal lit = literal().to(field.type()); if (lit == null) { throw new ValidationException(String.format( diff --git a/api/src/test/java/com/netflix/iceberg/expressions/TestEvaluatior.java b/api/src/test/java/com/netflix/iceberg/expressions/TestEvaluatior.java index 9b9a70f4a..27c127770 100644 --- a/api/src/test/java/com/netflix/iceberg/expressions/TestEvaluatior.java +++ b/api/src/test/java/com/netflix/iceberg/expressions/TestEvaluatior.java @@ -17,6 +17,7 @@ package com.netflix.iceberg.expressions; import com.netflix.iceberg.TestHelpers; +import com.netflix.iceberg.exceptions.ValidationException; import com.netflix.iceberg.types.Types; import com.netflix.iceberg.types.Types.StructType; import org.apache.avro.util.Utf8; @@ -26,6 +27,7 @@ import static com.netflix.iceberg.expressions.Expressions.alwaysFalse; import static com.netflix.iceberg.expressions.Expressions.alwaysTrue; import static com.netflix.iceberg.expressions.Expressions.and; +import static com.netflix.iceberg.expressions.Expressions.startsWith; import static com.netflix.iceberg.expressions.Expressions.equal; import static com.netflix.iceberg.expressions.Expressions.greaterThan; import static com.netflix.iceberg.expressions.Expressions.greaterThanOrEqual; @@ -151,4 +153,22 @@ public void testCharSeqValue() { Assert.assertFalse("string(abc) == utf8(abcd) => false", evaluator.eval(TestHelpers.Row.of(new Utf8("abcd")))); } + + @Test + public void testStartsWith() { + StructType struct = StructType.of(required(3, "s", Types.StringType.get())); + Evaluator evaluator = new Evaluator(struct, startsWith("s", "abc")); + Assert.assertTrue("startsWith(abcdddd, abc) => true", + evaluator.eval(TestHelpers.Row.of(("abcdddd")))); + + Assert.assertFalse("startsWith(xyzffff, abc) => false", + evaluator.eval(TestHelpers.Row.of(("xyzffff")))); + } + + @Test(expected = ValidationException.class) + public void testStartsWithThrowsOnNotString() { + StructType struct = StructType.of(required(3, "s", Types.IntegerType.get())); + Evaluator evaluator = new Evaluator(struct, startsWith("s", 112)); + evaluator.eval(TestHelpers.Row.of(("xyzffff"))); + } }