This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 627b8d9e806 Fix column-to-column comparison in WHERE clause (#18587)
627b8d9e806 is described below
commit 627b8d9e806bba84c99348e869de94d00291a037
Author: Samuel Papin <[email protected]>
AuthorDate: Wed Jul 1 18:26:08 2026 -0400
Fix column-to-column comparison in WHERE clause (#18587)
* Rewrite column-to-column comparison using type-safe comparison transforms
Previously PredicateComparisonRewriter rewrote column-to-column comparisons
like `WHERE a = b` into `minus(a, b) = 0`. That arithmetic rewrite forced
DOUBLE coercion on both operands, throwing NumberFormatException for any
non-numeric STRING value (e.g. a json_extract_scalar result).
This replaces the minus() rewrite with the type-safe comparison transform
functions (equals, not_equals, greater_than, ...), so `WHERE a = b` becomes
`WHERE equals(a, b) = true`. These dispatch on the stored types of both
operands (String.compareTo for STRING, Number.compare for numerics) with no
forced coercion, fixing both the single-stage and multi-stage engines.
Knock-on changes:
- IdenticalPredicateFilterOptimizer now folds the new `comparison(a, a) =
true`
form to a constant TRUE/FALSE (true for =, >=, <=; false for !=, >, <),
guarding against folding non-comparison functions such as
`startsWith(a, a) = true`. The folding logic is consolidated into a single
helper returning Optional<Boolean>. The previous handling of the legacy
`minus(a, a) = 0` / `!= 0` form is removed: comparisons no longer rewrite
to
minus, so that path was dead. (It also covers more cases than before,
which
only folded = and !=, never >=, <=, >, <.)
- Remove StringPredicateFilterOptimizer: it existed solely to repair the old
minus() rewrite for STRING columns by swapping in strcmp(). With the minus
form no longer generated for comparisons, it is dead code. The only
behavior
it still provided was treating explicit `strCol1 - strCol2 <op> 0` as a
string comparison, which was never an intended feature and now correctly
fails as a type error.
- Update affected optimizer/rewriter tests for the new comparison form, and
add coverage for folding >=, <=, >, < on identical operands.
* Add test for INT vs non-numeric STRING column comparison
Covers the behavior change from the type-safe comparison rewrite: comparing
an
INT column to a non-numeric STRING column no longer throws
NumberFormatException
and fails the query; it now evaluates per-row and returns no rows.
---------
Co-authored-by: Xiang Fu <[email protected]>
---
.../rewriter/PredicateComparisonRewriter.java | 35 +++++-
.../pinot/sql/parsers/CalciteSqlCompilerTest.java | 26 ++---
.../rewriter/PredicateComparisonRewriterTest.java | 116 +++++++++++--------
.../pinot/core/query/optimizer/QueryOptimizer.java | 3 +-
.../filter/IdenticalPredicateFilterOptimizer.java | 95 ++++++++--------
.../statement/StringPredicateFilterOptimizer.java | 124 ---------------------
.../core/query/optimizer/QueryOptimizerTest.java | 7 ++
.../StringPredicateFilterOptimizerTest.java | 68 -----------
.../BrokerRequestToQueryContextConverterTest.java | 14 +--
.../pinot/queries/JsonExtractScalarTest.java | 23 ++++
10 files changed, 196 insertions(+), 315 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriter.java
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriter.java
index 5b9fff5659f..208955e55f1 100644
---
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriter.java
+++
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriter.java
@@ -21,6 +21,7 @@ package org.apache.pinot.sql.parsers.rewriter;
import com.google.common.base.Preconditions;
import java.util.List;
import org.apache.commons.lang3.EnumUtils;
+import org.apache.pinot.common.function.TransformFunctionType;
import org.apache.pinot.common.request.Expression;
import org.apache.pinot.common.request.ExpressionType;
import org.apache.pinot.common.request.Function;
@@ -46,8 +47,10 @@ public class PredicateComparisonRewriter implements
QueryRewriter {
/**
* This method converts an expression to what Pinot could evaluate.
- * 1. For comparison expression, left operand could be any expression, but
right operand only
- * supports literal. E.g. 'WHERE a > b' will be converted to 'WHERE a - b
> 0'
+ * 1. For comparison expressions, the right operand should be a literal. If
the left operand is a
+ * literal and the right is not, they are swapped. If the right operand
is still non-literal
+ * (column-to-column comparison), the predicate is rewritten using a
comparison transform
+ * function: e.g. 'WHERE a = b' becomes 'WHERE equals(a, b) = true'.
* 2. Updates boolean predicates (literals and scalar functions) that are
missing an EQUALS filter.
* E.g. 1: 'WHERE a' will be updated to 'WHERE a = true'
* E.g. 2: "WHERE startsWith(col, 'str')" will be updated to "WHERE
startsWith(col, 'str') = true"
@@ -115,11 +118,12 @@ public class PredicateComparisonRewriter implements
QueryRewriter {
break;
}
- // Handle predicate like 'a > b' -> 'a - b > 0'
if (!secondOperand.isSetLiteral()) {
- Expression minusExpression =
RequestUtils.getFunctionExpression("minus", firstOperand, secondOperand);
- operands.set(0, minusExpression);
- operands.set(1, RequestUtils.getLiteralExpression(0));
+ Expression comparisonExpression =
RequestUtils.getFunctionExpression(
+ getComparisonFunctionName(filterKind), firstOperand,
secondOperand);
+ function.setOperator(FilterKind.EQUALS.name());
+ operands.set(0, comparisonExpression);
+ operands.set(1, RequestUtils.getLiteralExpression(true));
break;
}
break;
@@ -204,6 +208,25 @@ public class PredicateComparisonRewriter implements
QueryRewriter {
RequestUtils.getLiteralExpression(true));
}
+ private static String getComparisonFunctionName(FilterKind filterKind) {
+ switch (filterKind) {
+ case EQUALS:
+ return TransformFunctionType.EQUALS.getName();
+ case NOT_EQUALS:
+ return TransformFunctionType.NOT_EQUALS.getName();
+ case GREATER_THAN:
+ return TransformFunctionType.GREATER_THAN.getName();
+ case GREATER_THAN_OR_EQUAL:
+ return TransformFunctionType.GREATER_THAN_OR_EQUAL.getName();
+ case LESS_THAN:
+ return TransformFunctionType.LESS_THAN.getName();
+ case LESS_THAN_OR_EQUAL:
+ return TransformFunctionType.LESS_THAN_OR_EQUAL.getName();
+ default:
+ throw new IllegalStateException("Unsupported comparison operator: " +
filterKind);
+ }
+ }
+
/**
* The purpose of this method is to convert expression "0 < columnA" to
"columnA > 0".
* The conversion would be:
diff --git
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
index 9db25530264..cfc2aea8494 100644
---
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
@@ -492,13 +492,13 @@ public class CalciteSqlCompilerTest {
public void testFilterClausesWithRightExpression() {
PinotQuery pinotQuery = compileToPinotQuery("select * from vegetables
where a > b");
Function func = pinotQuery.getFilterExpression().getFunctionCall();
- Assert.assertEquals(func.getOperator(), FilterKind.GREATER_THAN.name());
-
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperator(),
"minus");
+ Assert.assertEquals(func.getOperator(), FilterKind.EQUALS.name());
+
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperator(),
"greater_than");
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"a");
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperands().get(1).getIdentifier().getName(),
"b");
- Assert.assertEquals(func.getOperands().get(1).getLiteral().getIntValue(),
0);
+ Assert.assertTrue(func.getOperands().get(1).getLiteral().getBoolValue());
pinotQuery = compileToPinotQuery("select * from vegetables where 0 < a-b");
func = pinotQuery.getFilterExpression().getFunctionCall();
Assert.assertEquals(func.getOperator(), FilterKind.GREATER_THAN.name());
@@ -511,8 +511,8 @@ public class CalciteSqlCompilerTest {
pinotQuery = compileToPinotQuery("select * from vegetables where b < 100 +
c");
func = pinotQuery.getFilterExpression().getFunctionCall();
- Assert.assertEquals(func.getOperator(), FilterKind.LESS_THAN.name());
-
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperator(),
"minus");
+ Assert.assertEquals(func.getOperator(), FilterKind.EQUALS.name());
+
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperator(),
"less_than");
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"b");
Assert.assertEquals(
@@ -523,7 +523,7 @@ public class CalciteSqlCompilerTest {
Assert.assertEquals(
func.getOperands().get(0).getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(1)
.getIdentifier().getName(), "c");
- Assert.assertEquals(func.getOperands().get(1).getLiteral().getIntValue(),
0);
+ Assert.assertTrue(func.getOperands().get(1).getLiteral().getBoolValue());
pinotQuery = compileToPinotQuery("select * from vegetables where b
-(100+c)< 0");
func = pinotQuery.getFilterExpression().getFunctionCall();
Assert.assertEquals(func.getOperator(), FilterKind.LESS_THAN.name());
@@ -542,8 +542,8 @@ public class CalciteSqlCompilerTest {
pinotQuery = compileToPinotQuery("select * from vegetables where
foo1(bar1(a-b)) <= foo2(bar2(c+d))");
func = pinotQuery.getFilterExpression().getFunctionCall();
- Assert.assertEquals(func.getOperator(),
FilterKind.LESS_THAN_OR_EQUAL.name());
-
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperator(),
"minus");
+ Assert.assertEquals(func.getOperator(), FilterKind.EQUALS.name());
+
Assert.assertEquals(func.getOperands().get(0).getFunctionCall().getOperator(),
"less_than_or_equal");
Assert.assertEquals(
func.getOperands().get(0).getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
"foo1");
Assert.assertEquals(
@@ -576,7 +576,7 @@ public class CalciteSqlCompilerTest {
func.getOperands().get(0).getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(0)
.getFunctionCall().getOperands().get(0).getFunctionCall().getOperands().get(1).getIdentifier().getName(),
"d");
- Assert.assertEquals(func.getOperands().get(1).getLiteral().getIntValue(),
0);
+ Assert.assertTrue(func.getOperands().get(1).getLiteral().getBoolValue());
pinotQuery = compileToPinotQuery("select * from vegetables where
foo1(bar1(a-b)) - foo2(bar2(c+d)) <= 0");
func = pinotQuery.getFilterExpression().getFunctionCall();
Assert.assertEquals(func.getOperator(),
FilterKind.LESS_THAN_OR_EQUAL.name());
@@ -3135,16 +3135,16 @@ public class CalciteSqlCompilerTest {
}
}
{
- // Having will be rewritten to (SUM(col1) + SUM(col3)) - MAX(col4) > 0
+ // Having will be rewritten to greaterThan(SUM(col1) + SUM(col3),
MAX(col4)) = true
String query = "SELECT SUM(col1), col2 FROM foo GROUP BY col2 HAVING
SUM(col1) + SUM(col3) > MAX(col4)";
PinotQuery pinotQuery = compileToPinotQuery(query);
Function functionCall =
pinotQuery.getHavingExpression().getFunctionCall();
- Assert.assertEquals(functionCall.getOperator(),
FilterKind.GREATER_THAN.name());
+ Assert.assertEquals(functionCall.getOperator(),
FilterKind.EQUALS.name());
List<Expression> operands = functionCall.getOperands();
Assert.assertEquals(operands.size(), 2);
- Assert.assertEquals(operands.get(1).getLiteral().getIntValue(), 0);
+ Assert.assertTrue(operands.get(1).getLiteral().getBoolValue());
functionCall = operands.get(0).getFunctionCall();
- Assert.assertEquals(functionCall.getOperator(), "minus");
+ Assert.assertEquals(functionCall.getOperator(), "greater_than");
operands = functionCall.getOperands();
Assert.assertEquals(operands.size(), 2);
Assert.assertEquals(operands.get(1).getFunctionCall().getOperator(),
"max");
diff --git
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriterTest.java
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriterTest.java
index 4a11a79d876..63cff46ad1a 100644
---
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriterTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/rewriter/PredicateComparisonRewriterTest.java
@@ -125,56 +125,78 @@ public class PredicateComparisonRewriterTest {
@Test
public void testFilterPredicateColumnComparisonRewrite() {
- // Filters like 'col1 = col2' should be rewritten to 'col1 - col2 = 0'
-
- PinotQuery pinotQuery =
- CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col1 = col2 AND col3 < col4;");
-
assertEquals(pinotQuery.getFilterExpression().getFunctionCall().getOperator(),
"AND");
-
assertEquals(pinotQuery.getFilterExpression().getFunctionCall().getOperands().size(),
2);
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
- "EQUALS");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands().get(0)
- .getIdentifier().getName(), "col1");
+ // col1 = col2 should be rewritten to equals(col1, col2) = true
+ PinotQuery equalsQuery =
+ CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col1 = col2");
+ PinotQuery rewrittenEquals =
_predicateComparisonRewriter.rewrite(equalsQuery);
+
assertEquals(rewrittenEquals.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands().get(1)
- .getIdentifier().getName(), "col2");
-
- PinotQuery rewrittenQuery =
_predicateComparisonRewriter.rewrite(pinotQuery);
-
assertEquals(rewrittenQuery.getFilterExpression().getFunctionCall().getOperator(),
"AND");
-
assertEquals(rewrittenQuery.getFilterExpression().getFunctionCall().getOperands().size(),
2);
+
rewrittenEquals.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "equals");
assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
- "EQUALS");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands().get(0)
- .getFunctionCall().getOperator(), "minus");
+
rewrittenEquals.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands()
+ .get(0).getIdentifier().getName(), "col1");
assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands().get(0)
- .getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"col1");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands().get(0)
- .getFunctionCall().getOperands().get(1).getIdentifier().getName(),
"col2");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(1)
- .getLiteral().getIntValue(), 0);
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getFunctionCall().getOperator(),
- "LESS_THAN");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(0)
- .getFunctionCall().getOperator(), "minus");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(0)
- .getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"col3");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(0)
- .getFunctionCall().getOperands().get(1).getIdentifier().getName(),
"col4");
- assertEquals(
-
pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getFunctionCall().getOperands().get(1)
- .getLiteral().getIntValue(), 0);
-
+
rewrittenEquals.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperands()
+ .get(1).getIdentifier().getName(), "col2");
+ assertTrue(
+
rewrittenEquals.getFilterExpression().getFunctionCall().getOperands().get(1).getLiteral().getBoolValue());
+
+ // col3 < col4 should be rewritten to less_than(col3, col4) = true
+ PinotQuery lessThanQuery =
+ CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col3 < col4");
+ PinotQuery rewrittenLt =
_predicateComparisonRewriter.rewrite(lessThanQuery);
+
assertEquals(rewrittenLt.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
+ assertEquals(
+
rewrittenLt.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "less_than");
+
+ // col1 != col2 should be rewritten to not_equals(col1, col2) = true
+ PinotQuery notEqualsQuery =
+ CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col1 != col2");
+ PinotQuery rewrittenNeq =
_predicateComparisonRewriter.rewrite(notEqualsQuery);
+
assertEquals(rewrittenNeq.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
+ assertEquals(
+
rewrittenNeq.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "not_equals");
+
+ // Function on LHS with column on RHS
+ PinotQuery functionRhsQuery =
CalciteSqlParser.compileToPinotQueryWithoutRewrites(
+ "SELECT * FROM mytable WHERE json_extract_scalar(col1, '$.f',
'STRING', 'null') = col2");
+ PinotQuery rewrittenFunc =
_predicateComparisonRewriter.rewrite(functionRhsQuery);
+
assertEquals(rewrittenFunc.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
+ assertEquals(
+
rewrittenFunc.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "equals");
+
+ // col5 >= col6 should be rewritten to greater_than_or_equal(col5, col6) =
true
+ PinotQuery gteQuery =
+ CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col5 >= col6");
+ PinotQuery rewrittenGte = _predicateComparisonRewriter.rewrite(gteQuery);
+
assertEquals(rewrittenGte.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
+ assertEquals(
+
rewrittenGte.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "greater_than_or_equal");
+
+ // col5 <= col6 should be rewritten to less_than_or_equal(col5, col6) =
true
+ PinotQuery lteQuery =
+ CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col5 <= col6");
+ PinotQuery rewrittenLte = _predicateComparisonRewriter.rewrite(lteQuery);
+
assertEquals(rewrittenLte.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
+ assertEquals(
+
rewrittenLte.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "less_than_or_equal");
+
+ // col7 > col8 should be rewritten to greater_than(col7, col8) = true
+ PinotQuery gtQuery =
+ CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col7 > col8");
+ PinotQuery rewrittenGt = _predicateComparisonRewriter.rewrite(gtQuery);
+
assertEquals(rewrittenGt.getFilterExpression().getFunctionCall().getOperator(),
"EQUALS");
+ assertEquals(
+
rewrittenGt.getFilterExpression().getFunctionCall().getOperands().get(0).getFunctionCall().getOperator(),
+ "greater_than");
+
+ // BETWEEN with non-literal bounds should still throw (not a comparison
operator)
PinotQuery betweenQuery =
CalciteSqlParser.compileToPinotQueryWithoutRewrites("SELECT * FROM
mytable WHERE col1 BETWEEN col2 AND col3");
assertThrows(SqlCompilationException.class, () ->
_predicateComparisonRewriter.rewrite(betweenQuery));
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
index 70151b7fec8..67e02f7abfd 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/QueryOptimizer.java
@@ -32,7 +32,6 @@ import
org.apache.pinot.core.query.optimizer.filter.TextMatchFilterOptimizer;
import
org.apache.pinot.core.query.optimizer.filter.TimePredicateFilterOptimizer;
import
org.apache.pinot.core.query.optimizer.statement.AggregateFunctionRewriteOptimizer;
import org.apache.pinot.core.query.optimizer.statement.StatementOptimizer;
-import
org.apache.pinot.core.query.optimizer.statement.StringPredicateFilterOptimizer;
import org.apache.pinot.spi.data.Schema;
@@ -49,7 +48,7 @@ public class QueryOptimizer {
new MergeRangeFilterOptimizer(), new TextMatchFilterOptimizer());
private static final List<StatementOptimizer> STATEMENT_OPTIMIZERS =
- List.of(new StringPredicateFilterOptimizer(), new
AggregateFunctionRewriteOptimizer());
+ List.of(new AggregateFunctionRewriteOptimizer());
/**
* Optimizes the given query.
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/IdenticalPredicateFilterOptimizer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/IdenticalPredicateFilterOptimizer.java
index ba6401799ba..720ab03c9d3 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/IdenticalPredicateFilterOptimizer.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/filter/IdenticalPredicateFilterOptimizer.java
@@ -19,7 +19,10 @@
package org.apache.pinot.core.query.optimizer.filter;
import java.util.List;
+import java.util.Optional;
+import java.util.Set;
import javax.annotation.Nullable;
+import org.apache.pinot.common.function.TransformFunctionType;
import org.apache.pinot.common.request.Expression;
import org.apache.pinot.common.request.Function;
import org.apache.pinot.common.request.Literal;
@@ -28,12 +31,27 @@ import org.apache.pinot.sql.FilterKind;
/**
- * This optimizer converts all predicates where the left hand side == right
hand side to
- * a simple TRUE/FALSE literal value. While filters like, WHERE 1=1 OR
"col1"="col1" are not
- * typical, they end up expensive in Pinot because they are rewritten as
A-A==0.
+ * This optimizer folds predicates that compare an expression to itself to a
constant TRUE/FALSE literal, so the engine
+ * does not evaluate them per-row. Such predicates are not typical to write by
hand (e.g. {@code WHERE col1 = col1}),
+ * but they are also produced internally and would otherwise be expensive.
+ *
+ * <p>{@code PredicateComparisonRewriter} rewrites a column-to-column
comparison {@code a <op> b} into
+ * {@code comparison(a, b) = true} (e.g. {@code equals(a, b) = true}). When
the two operands are the same expression,
+ * the inner comparison has a constant value, which this optimizer
substitutes: {@code true} for {@code =}, {@code >=},
+ * {@code <=} and {@code false} for {@code !=}, {@code >}, {@code <}.
*/
public class IdenticalPredicateFilterOptimizer extends
BaseAndOrBooleanFilterOptimizer {
+ // Comparison operators that are always true / always false when their two
operands are identical.
+ private static final Set<String> TRUE_FOR_IDENTICAL_OPERANDS = Set.of(
+ TransformFunctionType.EQUALS.getName(),
+ TransformFunctionType.GREATER_THAN_OR_EQUAL.getName(),
+ TransformFunctionType.LESS_THAN_OR_EQUAL.getName());
+ private static final Set<String> FALSE_FOR_IDENTICAL_OPERANDS = Set.of(
+ TransformFunctionType.NOT_EQUALS.getName(),
+ TransformFunctionType.GREATER_THAN.getName(),
+ TransformFunctionType.LESS_THAN.getName());
+
@Override
boolean canBeOptimized(Expression filterExpression, @Nullable Schema schema)
{
// if there's no function call, there's no lhs or rhs
@@ -43,62 +61,45 @@ public class IdenticalPredicateFilterOptimizer extends
BaseAndOrBooleanFilterOpt
@Override
Expression optimizeChild(Expression filterExpression, @Nullable Schema
schema) {
Function function = filterExpression.getFunctionCall();
- FilterKind kind = FilterKind.valueOf(function.getOperator());
- switch (kind) {
- case EQUALS:
- if (hasIdenticalLhsAndRhs(function.getOperands())) {
- return TRUE;
- }
- break;
- case NOT_EQUALS:
- if (hasIdenticalLhsAndRhs(function.getOperands())) {
- return FALSE;
- }
- break;
- default:
- break;
+ if (FilterKind.valueOf(function.getOperator()) == FilterKind.EQUALS) {
+ Optional<Boolean> folded =
foldIdenticalComparisonWithTrueLiteral(function.getOperands());
+ if (folded.isPresent()) {
+ return getExpressionFromBoolean(folded.get());
+ }
}
return filterExpression;
}
/**
- * Pinot queries of the WHERE 1 != 1 AND "col1" = "col2" variety are
rewritten as
- * 1-1 != 0 AND "col1"-"col2" = 0. Therefore, we check specifically for the
case where
- * the operand is set up in this fashion.
- *
- * We return false specifically after every check to ensure we're only
continuing when
- * the input looks as expected. Otherwise, it's easy to for one of the
operand functions
- * to return null and fail the query.
- *
- * TODO: The rewrite is already happening in
PredicateComparisonRewriter.updateFunctionExpression(),
- * so we might just compare the lhs and rhs there.
+ * Folds a predicate of the form 'comparison(a, a) = true' — a comparison
whose two operands are the <em>same</em>
+ * expression — to the constant value it must always have. Returns an empty
{@link Optional} when the predicate is
+ * not such a comparison: the operands differ, the right-hand side is not
the literal {@code true}, or the function
+ * is a non-comparison like 'startsWith(a, a) = true' whose value cannot be
determined here.
*/
- private boolean hasIdenticalLhsAndRhs(List<Expression> operands) {
- boolean hasTwoChildren = operands.size() == 2;
- Expression firstChild = operands.get(0);
- if (firstChild.getFunctionCall() == null || !hasTwoChildren) {
- return false;
+ private Optional<Boolean>
foldIdenticalComparisonWithTrueLiteral(List<Expression> operands) {
+ // The predicate must be 'comparison(a, b) = true'.
+ if (operands.size() != 2 || operands.get(0).getFunctionCall() == null ||
!isLiteralTrue(operands.get(1))) {
+ return Optional.empty();
}
- boolean firstChildIsMinusOperator =
firstChild.getFunctionCall().getOperator().equals("minus");
- if (!firstChildIsMinusOperator) {
- return false;
+ // The two compared operands must be the same expression, i.e. 'a <op> a'.
+ List<Expression> comparisonOperands =
operands.get(0).getFunctionCall().getOperands();
+ if (comparisonOperands.size() != 2 || comparisonOperands.get(0) == null
+ || !comparisonOperands.get(0).equals(comparisonOperands.get(1))) {
+ return Optional.empty();
}
- boolean firstChildHasTwoOperands =
firstChild.getFunctionCall().getOperandsSize() == 2;
- if (!firstChildHasTwoOperands) {
- return false;
+ // 'a <op> a' is constant for comparison operators: true for =, >=, <=;
false for !=, >, <.
+ String operator = operands.get(0).getFunctionCall().getOperator();
+ if (TRUE_FOR_IDENTICAL_OPERANDS.contains(operator)) {
+ return Optional.of(Boolean.TRUE);
}
- Expression minusOperandFirstChild =
firstChild.getFunctionCall().getOperands().get(0);
- Expression minusOperandSecondChild =
firstChild.getFunctionCall().getOperands().get(1);
- if (minusOperandFirstChild == null || minusOperandSecondChild == null ||
!minusOperandFirstChild.equals(
- minusOperandSecondChild)) {
- return false;
+ if (FALSE_FOR_IDENTICAL_OPERANDS.contains(operator)) {
+ return Optional.of(Boolean.FALSE);
}
- Expression secondChild = operands.get(1);
- return isLiteralZero(secondChild);
+ return Optional.empty();
}
- private boolean isLiteralZero(Expression expression) {
+ private boolean isLiteralTrue(Expression expression) {
Literal literal = expression.getLiteral();
- return literal != null && literal.isSetIntValue() && literal.getIntValue()
== 0;
+ return literal != null && literal.isSetBoolValue() &&
literal.getBoolValue();
}
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/StringPredicateFilterOptimizer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/StringPredicateFilterOptimizer.java
deleted file mode 100644
index 7e1eb6c540f..00000000000
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/StringPredicateFilterOptimizer.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.pinot.core.query.optimizer.statement;
-
-import java.util.List;
-import javax.annotation.Nullable;
-import org.apache.pinot.common.function.FunctionInfo;
-import org.apache.pinot.common.function.FunctionRegistry;
-import org.apache.pinot.common.request.Expression;
-import org.apache.pinot.common.request.ExpressionType;
-import org.apache.pinot.common.request.Function;
-import org.apache.pinot.common.request.PinotQuery;
-import org.apache.pinot.spi.data.FieldSpec;
-import org.apache.pinot.spi.data.Schema;
-import org.apache.pinot.sql.FilterKind;
-
-
-/**
- * Given two column names 'strColumn1' and 'strColumn1',
CalciteSqlParser.queryRewrite will turn WHERE and HAVING
- * expressions of form "strColumn1 <operator> strColumn2" into
"MINUS(strColumn1,strColumn2) <operator> 0" regardless
- * of the column datatype. The resulting query will fail to evaluate since the
MINUS operator does not work with the
- * STRING column type. This class rewrites expressions of form
"MINUS(strColumn1,strColumn2) <operator> 0" to
- * "STRCMP(strColumn1, strColumn2) <operator> 0" to fix the issue.
- *
- * Currently, rewrite phase (see CalciteSqlParser.queryRewrite) does not have
access to schema; hence, we need to again
- * rewrite MINUS(strColumn1, strColumn2) into STRCMP(strColumn1, strColumn2).
At some point, we should merge query
- * rewrite phase with optimizer phase to avoid such issues altogether.
- */
-public class StringPredicateFilterOptimizer implements StatementOptimizer {
- private static final String MINUS_OPERATOR_NAME = "minus";
- private static final String STRCMP_OPERATOR_NAME = "strcmp";
-
- @Override
- public void optimize(PinotQuery query, @Nullable Schema schema) {
- if (schema == null) {
- return;
- }
-
- Expression filter = query.getFilterExpression();
- if (filter != null) {
- optimizeExpression(filter, schema);
- }
-
- Expression expression = query.getHavingExpression();
- if (expression != null) {
- optimizeExpression(expression, schema);
- }
- }
-
- /** Traverse an expression tree to replace MINUS function with STRCMP if
function operands are STRING. */
- private static void optimizeExpression(Expression expression, Schema schema)
{
- ExpressionType type = expression.getType();
- if (type != ExpressionType.FUNCTION) {
- // We have nothing to rewrite if expression is not a function.
- return;
- }
-
- Function function = expression.getFunctionCall();
- String operator = function.getOperator();
- List<Expression> operands = function.getOperands();
- if (operator.equals(FilterKind.AND.name()) ||
operator.equals(FilterKind.OR.name()) || operator.equals(
- FilterKind.NOT.name())) {
- for (Expression operand : operands) {
- optimizeExpression(operand, schema);
- }
- } else {
- replaceMinusWithCompareForStrings(operands.get(0), schema);
- }
- }
-
- /** Replace the operator of a MINUS function with COMPARE if both operands
are STRING. */
- private static void replaceMinusWithCompareForStrings(Expression expression,
Schema schema) {
- if (expression.getType() != ExpressionType.FUNCTION) {
- // We have nothing to rewrite if expression is not a function.
- return;
- }
-
- Function function = expression.getFunctionCall();
- String operator = function.getOperator();
- List<Expression> operands = function.getOperands();
- if (operator.equals(MINUS_OPERATOR_NAME) && operands.size() == 2 &&
isString(operands.get(0), schema) && isString(
- operands.get(1), schema)) {
- function.setOperator(STRCMP_OPERATOR_NAME);
- }
- }
-
- /** @return true if expression is STRING column or a function that outputs
STRING. */
- private static boolean isString(Expression expression, Schema schema) {
- ExpressionType expressionType = expression.getType();
-
- if (expressionType == ExpressionType.IDENTIFIER) {
- // Check if this is a STRING column.
- String column = expression.getIdentifier().getName();
- FieldSpec fieldSpec = schema.getFieldSpecFor(column);
- return fieldSpec != null && fieldSpec.getDataType() ==
FieldSpec.DataType.STRING;
- }
-
- if (expressionType == ExpressionType.FUNCTION) {
- // Check if the function returns STRING as output.
- Function function = expression.getFunctionCall();
- String canonicalName =
FunctionRegistry.canonicalize(function.getOperator());
- FunctionInfo functionInfo =
FunctionRegistry.lookupFunctionInfo(canonicalName,
function.getOperands().size());
- return functionInfo != null && functionInfo.getMethod().getReturnType()
== String.class;
- }
-
- return false;
- }
-}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
index 9ab3ede7505..60ae769d024 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/QueryOptimizerTest.java
@@ -251,6 +251,10 @@ public class QueryOptimizerTest {
testQuery("SELECT * FROM testTable WHERE \"a\"=\"a\"", "SELECT * FROM
testTable WHERE true");
testQuery("SELECT * FROM testTable WHERE \"a\"!=\"a\"", "SELECT * FROM
testTable WHERE false");
+ testQuery("SELECT * FROM testTable WHERE \"a\">=\"a\"", "SELECT * FROM
testTable WHERE true");
+ testQuery("SELECT * FROM testTable WHERE \"a\"<=\"a\"", "SELECT * FROM
testTable WHERE true");
+ testQuery("SELECT * FROM testTable WHERE \"a\">\"a\"", "SELECT * FROM
testTable WHERE false");
+ testQuery("SELECT * FROM testTable WHERE \"a\"<\"a\"", "SELECT * FROM
testTable WHERE false");
testQuery("SELECT * FROM testTable WHERE \"a\"=\"a\" AND \"a\"!=\"a\"",
"SELECT * FROM testTable WHERE false");
testQuery("SELECT * FROM testTable WHERE \"a\"=\"a\" OR \"a\"!=\"a\"",
"SELECT * FROM testTable WHERE true");
@@ -268,6 +272,9 @@ public class QueryOptimizerTest {
testQuery("SELECT * FROM testTable WHERE 1=1 AND true", "SELECT * FROM
testTable WHERE true");
testQuery("SELECT * FROM testTable WHERE \"a\"=\"a\" AND true", "SELECT *
FROM testTable WHERE true");
+ // Non-comparison boolean functions with identical column arguments must
not be folded
+ testCannotOptimizeQuery("SELECT * FROM testTable WHERE startsWith(string,
string)");
+
// TextMatchFilterOptimizer
testQuery("SELECT * FROM testTable WHERE TEXT_MATCH(string, 'foo') AND
TEXT_MATCH(string, 'bar')",
"SELECT * FROM testTable WHERE TEXT_MATCH(string, '(foo) AND (bar)')");
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/StringPredicateFilterOptimizerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/StringPredicateFilterOptimizerTest.java
deleted file mode 100644
index 9f9ad2ac881..00000000000
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/StringPredicateFilterOptimizerTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.pinot.core.query.optimizer.statement;
-
-import org.apache.pinot.spi.config.table.TableConfig;
-import org.apache.pinot.spi.data.FieldSpec;
-import org.apache.pinot.spi.data.Schema;
-import org.testng.annotations.Test;
-
-
-public class StringPredicateFilterOptimizerTest {
- private static final Schema SCHEMA = new
Schema.SchemaBuilder().setSchemaName("testTable")
- .addSingleValueDimension("intColumn1", FieldSpec.DataType.INT)
- .addSingleValueDimension("intColumn2", FieldSpec.DataType.INT)
- .addSingleValueDimension("strColumn1", FieldSpec.DataType.STRING)
- .addSingleValueDimension("strColumn2", FieldSpec.DataType.STRING)
- .addSingleValueDimension("strColumn3",
FieldSpec.DataType.STRING).build();
- private static final TableConfig TABLE_CONFIG_WITHOUT_INDEX = null;
-
- @Test
- public void testReplaceMinusWithCompare() {
- // 'WHERE strColumn1=strColumn2' gets replaced with 'strcmp(strColumn1,
strColumn2) = 0'
- TestHelper.assertEqualsQuery("SELECT * FROM testTable WHERE
strColumn1=strColumn2",
- "SELECT * FROM testTable WHERE strcmp(strColumn1,strColumn2) = 0",
SCHEMA);
-
- // 'WHERE trim(strColumn1)=strColumn2' gets replaced with
'strcmp(trim(strColumn1), strColumn2) = 0'
- TestHelper.assertEqualsQuery("SELECT * FROM testTable WHERE
trim(strColumn1)=strColumn2",
- "SELECT * FROM testTable WHERE strcmp(trim(strColumn1),strColumn2) =
0", SCHEMA);
-
- // 'WHERE strColumn1=trim(strColumn2)' gets replaced with
'strcmp(strColumn1, trim(strColumn2)) = 0'
- TestHelper.assertEqualsQuery("SELECT * FROM testTable WHERE
strColumn1=trim(strColumn2)",
- "SELECT * FROM testTable WHERE strcmp(strColumn1,trim(strColumn2)) =
0", SCHEMA);
-
- // 'WHERE strColumn1>strColumn2' gets replaced with 'strcmp(strColumn1,
strColumn2) > 0'
- TestHelper.assertEqualsQuery("SELECT * FROM testTable WHERE
strColumn1>strColumn2",
- "SELECT * FROM testTable WHERE strcmp(strColumn1,strColumn2) > 0",
SCHEMA);
-
- // 'HAVING strColumn1=strColumn2' gets replaced with 'strcmp(strColumn1,
strColumn2) = 0'
- TestHelper.assertEqualsQuery("SELECT strColumn1, strColumn2 FROM testTable
HAVING strColumn1=strColumn2",
- "SELECT strColumn1, strColumn2 FROM testTable HAVING
strcmp(strColumn1,strColumn2)=0", SCHEMA);
-
- // 'HAVING strColumn1=strColumn2' gets replaced with 'strcmp(strColumn1,
strColumn2) < 0'
- TestHelper.assertEqualsQuery("SELECT strColumn1, strColumn2 FROM testTable
HAVING strColumn1<strColumn2",
- "SELECT strColumn1, strColumn2 FROM testTable HAVING
strcmp(strColumn1,strColumn2)<0", SCHEMA);
-
- // 'WHERE strColumn1=strColumn2 AND strColumn1=strColumn3' gets replaced
with 'strcmp(strColumn1, strColumn2) = 0
- // AND strcmp(strColumn1, strColumn3) = 0'
- TestHelper.assertEqualsQuery("SELECT * FROM testTable WHERE
strColumn1=strColumn2 OR strColumn1=strColumn3",
- "SELECT * FROM testTable WHERE strcmp(strColumn1,strColumn2) = 0 OR
strcmp(strColumn1,strColumn3) = 0",
- SCHEMA);
- }
-}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/request/context/utils/BrokerRequestToQueryContextConverterTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/request/context/utils/BrokerRequestToQueryContextConverterTest.java
index a36f0e75e8d..3639d470c34 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/request/context/utils/BrokerRequestToQueryContextConverterTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/request/context/utils/BrokerRequestToQueryContextConverterTest.java
@@ -28,6 +28,7 @@ import
org.apache.pinot.common.request.context.ExpressionContext;
import org.apache.pinot.common.request.context.FilterContext;
import org.apache.pinot.common.request.context.FunctionContext;
import org.apache.pinot.common.request.context.OrderByExpressionContext;
+import org.apache.pinot.common.request.context.predicate.EqPredicate;
import org.apache.pinot.common.request.context.predicate.InPredicate;
import org.apache.pinot.common.request.context.predicate.Predicate;
import org.apache.pinot.common.request.context.predicate.RangePredicate;
@@ -432,14 +433,11 @@ public class BrokerRequestToQueryContextConverterTest {
FilterContext firstChild = children.get(0);
assertEquals(firstChild.getType(), FilterContext.Type.PREDICATE);
Predicate predicate = firstChild.getPredicate();
- assertEquals(predicate.getType(), Predicate.Type.RANGE);
- RangePredicate rangePredicate = (RangePredicate) predicate;
- assertEquals(rangePredicate.getLowerBound(), "0");
- assertFalse(rangePredicate.isLowerInclusive());
- assertEquals(rangePredicate.getUpperBound(), RangePredicate.UNBOUNDED);
- assertFalse(rangePredicate.isUpperInclusive());
- function = rangePredicate.getLhs().getFunction();
- assertEquals(function.getFunctionName(), "minus");
+ assertEquals(predicate.getType(), Predicate.Type.EQ);
+ EqPredicate eqPredicate = (EqPredicate) predicate;
+ assertEquals(eqPredicate.getValue(), "true");
+ function = eqPredicate.getLhs().getFunction();
+ assertEquals(function.getFunctionName(), "greater_than");
arguments = function.getArguments();
assertEquals(arguments.size(), 2);
assertEquals(arguments.get(0), ExpressionContext.forFunction(
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/JsonExtractScalarTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/JsonExtractScalarTest.java
index 5c7049369a0..a16a9622b04 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/JsonExtractScalarTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/JsonExtractScalarTest.java
@@ -191,6 +191,29 @@ public class JsonExtractScalarTest extends
BaseJsonQueryTest {
);
}
+ @Test(dataProvider = "allJsonColumns")
+ public void testColumnToColumnComparison(String column) {
+ // A JSON last name ("duck", "mouse", ...) is compared as a STRING against
stringColumn ("daffy duck",
+ // "mickey mouse", ...). The two are never equal, so '=' matches no row
and '!=' matches every row.
+ String lastName = "json_extract_scalar(" + column + ", '$.name.last',
'STRING', '')";
+ checkResult("SELECT intColumn FROM testTable WHERE " + lastName + " =
stringColumn", new Object[][]{});
+ checkResult("SELECT intColumn FROM testTable WHERE " + lastName + " !=
stringColumn LIMIT 3",
+ new Object[][]{{1}, {2}, {3}});
+
+ // A JSON id (101, 111, 121, ... for the first rows) is compared
numerically against intColumn (1, 2, 3, ...).
+ // The id is never equal to the row's intColumn, and for the leading rows
it is the larger value.
+ String id = "json_extract_scalar(" + column + ", '$.id', 'INT', '0')";
+ checkResult("SELECT intColumn FROM testTable WHERE " + id + " =
intColumn", new Object[][]{});
+ checkResult("SELECT intColumn FROM testTable WHERE " + id + " > intColumn
LIMIT 3",
+ new Object[][]{{1}, {2}, {3}});
+
+ // Mixed numeric/string comparison: an INT column against a non-numeric
STRING column. The old minus()-based
+ // rewrite coerced both operands to DOUBLE and threw NumberFormatException
on the non-numeric string, failing the
+ // whole query. The type-safe rewrite evaluates per-row, treating an
unparseable comparison as no-match, so the
+ // query runs and simply returns no rows instead of erroring.
+ checkResult("SELECT intColumn FROM testTable WHERE intColumn =
stringColumn", new Object[][]{});
+ }
+
@Test
public void testNullAsDefaultValueWithNullHandlingEnabled() {
checkResult(
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]