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]


Reply via email to