From 0deef042b18689fd4b73f4b252700dd2f1ab94f8 Mon Sep 17 00:00:00 2001 From: Krishnan Paranji Ravi Date: Thu, 23 May 2024 11:45:39 -0400 Subject: [PATCH] [Kernel][Expressions] Add support for LIKE expression (#3103) ## Description Add SQL `LIKE` expression support in Kernel list of supported expressions and a default implementation. Addresses part of https://github.com/delta-io/delta/issues/2539 (where `STARTS_WITH` as `LIKE 'str%'`) ## How was this patch tested? added unit tests Signed-off-by: Krishnan Paranji Ravi --- .../delta/kernel/expressions/Predicate.java | 6 + .../internal/DefaultEngineErrors.java | 11 + .../DefaultExpressionEvaluator.java | 27 ++- .../expressions/ExpressionVisitor.java | 8 +- .../expressions/LikeExpressionEvaluator.java | 182 ++++++++++++++++ .../DefaultExpressionEvaluatorSuite.scala | 201 ++++++++++++++++++ .../expressions/ExpressionSuiteBase.scala | 13 ++ 7 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/LikeExpressionEvaluator.java diff --git a/kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Predicate.java b/kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Predicate.java index b36cee0b3e3..3b147d9e32a 100644 --- a/kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Predicate.java +++ b/kernel/kernel-api/src/main/java/io/delta/kernel/expressions/Predicate.java @@ -103,6 +103,12 @@ *
  • Since version: 3.2.0
  • * * + *
  • Name: LIKE + *
      + *
    • SQL semantic: expr LIKE expr
    • + *
    • Since version: 3.3.0
    • + *
    + *
  • * * * @since 3.0.0 diff --git a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/DefaultEngineErrors.java b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/DefaultEngineErrors.java index 74eedba0f8e..58f55d987f0 100644 --- a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/DefaultEngineErrors.java +++ b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/DefaultEngineErrors.java @@ -39,4 +39,15 @@ public static UnsupportedOperationException unsupportedExpressionException( reason); return new UnsupportedOperationException(message); } + + /** + * Exception class for invalid escape sequence used in input for LIKE expressions + * @param pattern the invalid pattern + * @param index character index of occurrence of the offending escape in the pattern + */ + public static IllegalArgumentException invalidEscapeSequence(String pattern, int index) { + return new IllegalArgumentException( + format("LIKE expression has invalid escape sequence '%s' at index %d", + pattern, index)); + } } diff --git a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluator.java b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluator.java index b873109d355..4328f080d47 100644 --- a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluator.java +++ b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluator.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import static java.lang.String.format; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; import io.delta.kernel.data.ColumnVector; import io.delta.kernel.data.ColumnarBatch; @@ -31,8 +32,6 @@ import static io.delta.kernel.internal.util.ExpressionUtils.getRight; import static io.delta.kernel.internal.util.ExpressionUtils.getUnaryChild; import static io.delta.kernel.internal.util.Preconditions.checkArgument; - - import io.delta.kernel.defaults.internal.data.vector.DefaultBooleanVector; import io.delta.kernel.defaults.internal.data.vector.DefaultConstantVector; import static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException; @@ -280,6 +279,21 @@ ExpressionTransformResult visitCoalesce(ScalarExpression coalesce) { ); } + @Override + ExpressionTransformResult visitLike(final Predicate like) { + List children = + like.getChildren().stream() + .map(this::visit) + .collect(toList()); + Predicate transformedExpression = + LikeExpressionEvaluator.validateAndTransform( + like, + children.stream().map(e -> e.expression).collect(toList()), + children.stream().map(e -> e.outputType).collect(toList())); + + return new ExpressionTransformResult(transformedExpression, BooleanType.BOOLEAN); + } + private Predicate validateIsPredicate( Expression baseExpression, ExpressionTransformResult result) { @@ -560,6 +574,15 @@ ColumnVector visitCoalesce(ScalarExpression coalesce) { ); } + @Override + ColumnVector visitLike(final Predicate like) { + List children = like.getChildren(); + return LikeExpressionEvaluator.eval( + children.stream() + .map(this::visit) + .collect(toList())); + } + /** * Utility method to evaluate inputs to the binary input expression. Also validates the * evaluated expression result {@link ColumnVector}s are of the same size. diff --git a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ExpressionVisitor.java b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ExpressionVisitor.java index bd219f55fda..01888d6f77f 100644 --- a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ExpressionVisitor.java +++ b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/ExpressionVisitor.java @@ -59,6 +59,8 @@ abstract class ExpressionVisitor { abstract R visitCoalesce(ScalarExpression ifNull); + abstract R visitLike(Predicate predicate); + final R visit(Expression expression) { if (expression instanceof PartitionValueExpression) { return visitPartitionValue((PartitionValueExpression) expression); @@ -105,6 +107,8 @@ private R visitScalarExpression(ScalarExpression expression) { return visitIsNull(new Predicate(name, children)); case "COALESCE": return visitCoalesce(expression); + case "LIKE": + return visitLike(new Predicate(name, children)); default: throw new UnsupportedOperationException( String.format("Scalar expression `%s` is not supported.", name)); @@ -114,8 +118,8 @@ private R visitScalarExpression(ScalarExpression expression) { private static Predicate elemAsPredicate(List expressions, int index) { if (expressions.size() <= index) { throw new RuntimeException( - String.format("Trying to access invalid entry (%d) in list %s", index, - expressions.stream().map(Object::toString).collect(joining(",")))); + String.format("Trying to access invalid entry (%d) in list %s", index, + expressions.stream().map(Object::toString).collect(joining(",")))); } Expression elemExpression = expressions.get(index); if (!(elemExpression instanceof Predicate)) { diff --git a/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/LikeExpressionEvaluator.java b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/LikeExpressionEvaluator.java new file mode 100644 index 00000000000..2070b4aacc0 --- /dev/null +++ b/kernel/kernel-defaults/src/main/java/io/delta/kernel/defaults/internal/expressions/LikeExpressionEvaluator.java @@ -0,0 +1,182 @@ +/* + * Copyright (2023) The Delta Lake Project Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.delta.kernel.defaults.internal.expressions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.expressions.Expression; +import io.delta.kernel.expressions.Literal; +import io.delta.kernel.expressions.Predicate; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.internal.util.Utils; + +import static io.delta.kernel.defaults.internal.DefaultEngineErrors.invalidEscapeSequence; +import static io.delta.kernel.defaults.internal.DefaultEngineErrors.unsupportedExpressionException; + +/** + * Utility methods to evaluate {@code like} expression. + */ +public class LikeExpressionEvaluator { + private LikeExpressionEvaluator() { + } + + static Predicate validateAndTransform( + Predicate like, + List childrenExpressions, + List childrenOutputTypes) { + int size = childrenExpressions.size(); + if (size < 2 || size > 3) { + throw unsupportedExpressionException(like, + "Invalid number of inputs to LIKE expression. " + + "Example usage: LIKE(column, 'test%'), LIKE(column, 'test\\[%', '\\')"); + } + + Expression left = childrenExpressions.get(0); + DataType leftOutputType = childrenOutputTypes.get(0); + Expression right = childrenExpressions.get(1); + DataType rightOutputType = childrenOutputTypes.get(1); + Expression escapeCharExpr = size == 3 ? childrenExpressions.get(2) : null; + DataType escapeCharOutputType = size == 3 ? childrenOutputTypes.get(2) : null; + + if (!(StringType.STRING.equivalent(leftOutputType) + && StringType.STRING.equivalent(rightOutputType))) { + throw unsupportedExpressionException(like, + "LIKE is only supported for string type expressions"); + } + + if (escapeCharExpr != null && + (!(escapeCharExpr instanceof Literal && + StringType.STRING.equivalent(escapeCharOutputType)))) { + throw unsupportedExpressionException(like, + "LIKE expects escape token expression to be a literal of String type"); + } + + Literal literal = (Literal) escapeCharExpr; + if (literal != null && + literal.getValue().toString().length() != 1) { + throw unsupportedExpressionException(like, + "LIKE expects escape token to be a single character"); + } + + List children = new ArrayList<>(Arrays.asList(left, right)); + if(Objects.nonNull(escapeCharExpr)) { + children.add(escapeCharExpr); + } + return new Predicate(like.getName(), children); + } + + static ColumnVector eval(List children) { + final char DEFAULT_ESCAPE_CHAR = '\\'; + + return new ColumnVector() { + final ColumnVector escapeCharVector = + children.size() == 3 ? + children.get(2) : + null; + final ColumnVector left = children.get(0); + final ColumnVector right = children.get(1); + + Character escapeChar = null; + + public void initEscapeCharIfRequired() { + if (escapeChar == null) { + escapeChar = + escapeCharVector != null && !escapeCharVector.getString(0).isEmpty() ? + escapeCharVector.getString(0).charAt(0) : + DEFAULT_ESCAPE_CHAR; + } + } + + @Override + public DataType getDataType() { + return BooleanType.BOOLEAN; + } + + @Override + public int getSize() { + return left.getSize(); + } + + @Override + public void close() { + Utils.closeCloseables(left, right); + } + + @Override + public boolean getBoolean(int rowId) { + initEscapeCharIfRequired(); + return isLike(left.getString(rowId), right.getString(rowId), escapeChar); + } + + @Override + public boolean isNullAt(int rowId) { + return left.isNullAt(rowId) || right.isNullAt(rowId); + } + + public boolean isLike(String input, String pattern, char escape) { + if (!Objects.isNull(input) && !Objects.isNull(pattern)) { + String regex = escapeLikeRegex(pattern, escape); + return input.matches(regex); + } + return false; + } + }; + } + + /** + * utility method to convert a predicate pattern to a java regex + * @param pattern the pattern used in the expression + * @param escape escape character to use + * @return java regex + */ + private static String escapeLikeRegex(String pattern, char escape) { + final int len = pattern.length(); + final StringBuilder javaPattern = new StringBuilder(len + len); + for (int i = 0; i < len; i++) { + char c = pattern.charAt(i); + + if (c == escape) { + if (i == (pattern.length() - 1)) { + throw invalidEscapeSequence(pattern, i); + } + char nextChar = pattern.charAt(i + 1); + if ((nextChar == '_') + || (nextChar == '%') + || (nextChar == escape)) { + javaPattern.append(Pattern.quote(Character.toString(nextChar))); + i++; + } else { + throw invalidEscapeSequence(pattern, i); + } + } else if (c == '_') { + javaPattern.append('.'); + } else if (c == '%') { + javaPattern.append(".*"); + } else { + javaPattern.append(Pattern.quote(Character.toString(c))); + } + + } + return "(?s)" + javaPattern; + } +} diff --git a/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluatorSuite.scala b/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluatorSuite.scala index 5502e88c556..cea162cbfc9 100644 --- a/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluatorSuite.scala +++ b/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/DefaultExpressionEvaluatorSuite.scala @@ -307,6 +307,207 @@ class DefaultExpressionEvaluatorSuite extends AnyFunSuite with ExpressionSuiteBa "Coalesce is only supported for boolean type expressions") } + test("evaluate expression: like") { + val col1 = stringVector(Seq[String]( + "one", "two", "three", "four", null, null, "seven", "eight")) + val col2 = stringVector(Seq[String]( + "one", "Two", "thr%", "four%", "f", null, null, "%ght")) + val schema = new StructType() + .add("col1", StringType.STRING) + .add("col2", StringType.STRING) + val input = new DefaultColumnarBatch(col1.getSize, schema, Array(col1, col2)) + + def checkLike( + input: DefaultColumnarBatch, + likeExpression: Predicate, + expOutputSeq: Seq[BooleanJ]): Unit = { + val actOutputVector = + new DefaultExpressionEvaluator( + schema, likeExpression, BooleanType.BOOLEAN).eval(input) + val expOutputVector = booleanVector(expOutputSeq); + checkBooleanVectors(actOutputVector, expOutputVector) + } + + // check column expressions on both sides + checkLike( + input, + like(new Column("col1"), new Column("col2")), + Seq[BooleanJ](true, false, true, true, null, null, null, true)) + + // check column expression against literal + checkLike( + input, + like(new Column("col1"), Literal.ofString("t%")), + Seq[BooleanJ](false, true, true, false, null, null, false, false)) + + // ends with checks + checkLike( + input, + like(new Column("col1"), Literal.ofString("%t")), + Seq[BooleanJ](false, false, false, false, null, null, false, true)) + + // contains checks + checkLike( + input, + like(new Column("col1"), Literal.ofString("%t%")), + Seq[BooleanJ](false, true, true, false, null, null, false, true)) + + val dummyInput = new DefaultColumnarBatch(1, + new StructType().add("dummy", StringType.STRING), + Array(stringVector(Seq[String]("")))) + + def checkLikeLiteral(left: String, right: String, + escape: Character = null, expOutput: BooleanJ): Unit = { + val expression = like(Literal.ofString(left), Literal.ofString(right), Option(escape)) + checkLike(dummyInput, expression, Seq[BooleanJ](expOutput)) + } + + // null/empty + checkLikeLiteral(null, "a", null, null) + checkLikeLiteral("a", null, null, null) + checkLikeLiteral(null, null, null, null) + checkLikeLiteral("", "", null, true) + checkLikeLiteral("a", "", null, false) + checkLikeLiteral("", "a", null, false) + + Seq('!', '@', '#').foreach { + escape => { + // simple patterns + checkLikeLiteral("abc", "abc", escape, true) + checkLikeLiteral("a_%b", s"a${escape}__b", escape, true) + checkLikeLiteral("abbc", "a_%c", escape, true) + checkLikeLiteral("abbc", s"a${escape}__c", escape, false) + checkLikeLiteral("abbc", s"a%${escape}%c", escape, false) + checkLikeLiteral("a_%b", s"a%${escape}%b", escape, true) + checkLikeLiteral("abbc", "a%", escape, true) + checkLikeLiteral("abbc", "**", escape, false) + checkLikeLiteral("abc", "a%", escape, true) + checkLikeLiteral("abc", "b%", escape, false) + checkLikeLiteral("abc", "bc%", escape, false) + checkLikeLiteral("a\nb", "a_b", escape, true) + checkLikeLiteral("ab", "a%b", escape, true) + checkLikeLiteral("a\nb", "a%b", escape, true) + checkLikeLiteral("a\nb", "ab", escape, false) + checkLikeLiteral("a\nb", "a\nb", escape, true) + checkLikeLiteral("a\n\nb", "a\nb", escape, false) + checkLikeLiteral("a\n\nb", "a\n_b", escape, true) + + // case + checkLikeLiteral("A", "a%", escape, false) + checkLikeLiteral("a", "a%", escape, true) + checkLikeLiteral("a", "A%", escape, false) + checkLikeLiteral(s"aAa", s"aA_", escape, true) + + // regex + checkLikeLiteral("a([a-b]{2,4})a", "_([a-b]{2,4})%", null, true) + checkLikeLiteral("a([a-b]{2,4})a", "_([a-c]{2,6})_", null, false) + + // %/_ + checkLikeLiteral("a%a", s"%${escape}%%", escape, true) + checkLikeLiteral("a%", s"%${escape}%%", escape, true) + checkLikeLiteral("a%a", s"_${escape}%_", escape, true) + checkLikeLiteral("a_a", s"%${escape}_%", escape, true) + checkLikeLiteral("a_", s"%${escape}_%", escape, true) + checkLikeLiteral("a_a", s"_${escape}__", escape, true) + + // double-escaping + checkLikeLiteral( + s"$escape$escape$escape$escape", s"%${escape}${escape}%", escape, true) + checkLikeLiteral("%%", "%%", escape, true) + checkLikeLiteral(s"${escape}__", s"${escape}${escape}${escape}__", escape, true) + checkLikeLiteral(s"${escape}__", s"%${escape}${escape}%${escape}%", escape, false) + checkLikeLiteral(s"_${escape}${escape}${escape}%", + s"%${escape}${escape}", escape, false) + } + } + + // check '_' for escape char + checkLikeLiteral("abc", "abc", '_', true) + checkLikeLiteral("a_%b", s"a__%%b", '_', true) + checkLikeLiteral("abbc", "a__c", '_', false) + checkLikeLiteral("abbc", "a%%c", '_', true) + checkLikeLiteral("abbc", s"a___%c", '_', false) + checkLikeLiteral("abbc", s"a%_%c", '_', false) + + // check '%' for escape char + checkLikeLiteral("abc", "abc", '%', true) + checkLikeLiteral("a_%b", s"a__%%b", '%', false) + checkLikeLiteral("a_%b", s"a_%%b", '%', true) + checkLikeLiteral("abbc", "a__c", '%', true) + checkLikeLiteral("abbc", "a%%c", '%', false) + checkLikeLiteral("abbc", s"a%__c", '%', false) + checkLikeLiteral("abbc", s"a%_%_c", '%', false) + + def checkUnsupportedTypes( + col1Type: DataType, col2Type: DataType): Unit = { + val schema = new StructType() + .add("col1", col1Type) + .add("col2", col2Type) + val expr = like(new Column("col1"), new Column("col2"), Option(null)) + val input = new DefaultColumnarBatch(5, schema, + Array(testColumnVector(5, col1Type), testColumnVector(5, col2Type))) + + val e = intercept[UnsupportedOperationException] { + new DefaultExpressionEvaluator( + schema, expr, BooleanType.BOOLEAN).eval(input) + } + assert(e.getMessage.contains("LIKE is only supported for string type expressions")) + } + checkUnsupportedTypes(BooleanType.BOOLEAN, BooleanType.BOOLEAN) + checkUnsupportedTypes(LongType.LONG, LongType.LONG) + checkUnsupportedTypes(IntegerType.INTEGER, IntegerType.INTEGER) + checkUnsupportedTypes(StringType.STRING, BooleanType.BOOLEAN) + checkUnsupportedTypes(StringType.STRING, IntegerType.INTEGER) + checkUnsupportedTypes(StringType.STRING, LongType.LONG) + checkUnsupportedTypes(BooleanType.BOOLEAN, BooleanType.BOOLEAN) + + // input count checks + val inputCountCheckUserMessage = + "Invalid number of inputs to LIKE expression. Example usage:" + val inputCountError1 = intercept[UnsupportedOperationException] { + val expression = like(List(Literal.ofString("a"))) + checkLike(dummyInput, expression, Seq[BooleanJ](null)) + } + assert(inputCountError1.getMessage.contains(inputCountCheckUserMessage)) + + val inputCountError2 = intercept[UnsupportedOperationException] { + val expression = like(List(Literal.ofString("a"), Literal.ofString("b"), + Literal.ofString("c"), Literal.ofString("d"))) + checkLike(dummyInput, expression, Seq[BooleanJ](null)) + } + assert(inputCountError2.getMessage.contains(inputCountCheckUserMessage)) + + // additional escape token checks + val escapeCharError1 = intercept[UnsupportedOperationException] { + val expression = + like(List(Literal.ofString("a"), Literal.ofString("b"), Literal.ofString("~~"))) + checkLike(dummyInput, expression, Seq[BooleanJ](null)) + } + assert(escapeCharError1.getMessage.contains( + "LIKE expects escape token to be a single character")) + + val escapeCharError2 = intercept[UnsupportedOperationException] { + val expression = like(List(Literal.ofString("a"), Literal.ofString("b"), Literal.ofInt(1))) + checkLike(dummyInput, expression, Seq[BooleanJ](null)) + } + assert(escapeCharError2.getMessage.contains( + "LIKE expects escape token expression to be a literal of String type")) + + // empty input checks + val emptyInput = new DefaultColumnarBatch(0, + new StructType().add("dummy", StringType.STRING), + Array(stringVector(Seq[String]("")))) + checkLike(emptyInput, + like(Literal.ofString("abc"), Literal.ofString("abc"), Some('_')), Seq[BooleanJ]()) + + // invalid pattern check + val invalidPatternError = intercept[IllegalArgumentException] { + checkLikeLiteral("abbc", "a%%%c", '%', false) + } + assert(invalidPatternError.getMessage.contains( + "LIKE expression has invalid escape sequence")) + } + test("evaluate expression: comparators (=, <, <=, >, >=)") { // Literals for each data type from the data type value range, used as inputs to comparator // (small, big, small, null) diff --git a/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/ExpressionSuiteBase.scala b/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/ExpressionSuiteBase.scala index 9a42192a29f..cbb28fd325f 100644 --- a/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/ExpressionSuiteBase.scala +++ b/kernel/kernel-defaults/src/test/scala/io/delta/kernel/defaults/internal/expressions/ExpressionSuiteBase.scala @@ -21,6 +21,8 @@ import io.delta.kernel.defaults.utils.{DefaultVectorTestUtils, TestUtils} import io.delta.kernel.expressions._ import io.delta.kernel.types._ +import scala.collection.JavaConverters._ + trait ExpressionSuiteBase extends TestUtils with DefaultVectorTestUtils { /** create a columnar batch of given `size` with zero columns in it. */ protected def zeroColumnBatch(rowCount: Int): ColumnarBatch = { @@ -35,6 +37,17 @@ trait ExpressionSuiteBase extends TestUtils with DefaultVectorTestUtils { new Or(left, right) } + protected def like( + left: Expression, right: Expression, escape: Option[Character] = None): Predicate = { + if (escape.isDefined && escape.get!=null) { + like(List(left, right, Literal.ofString(escape.get.toString))) + } else like(List(left, right)) + } + + protected def like(children: List[Expression]): Predicate = { + new Predicate("like", children.asJava) + } + protected def comparator(symbol: String, left: Expression, right: Expression): Predicate = { new Predicate(symbol, left, right) }