Repository: apache/calcite
Pull request #4214: [CALCITE-6835] Invalid unparse for IS TRUE,IS FALSE,IS NOT TRUE and I…
Author: login: NobiGo
Labels: name: LGTM-will-merge-soon
description: Overall PR looks OK. Only minor things left.
Created: 2025-02-26T02:14:19Z | Merged: 2025-04-07T01:31:28Z

…S NOT FALSE in StarRocksDialect


Diff:
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index a0733b69244d..d8e38d02d1b4 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -166,8 +166,12 @@ public abstract class SqlImplementor {
 
   protected final Map<CorrelationId, Context> correlTableMap = new HashMap<>();
 
-  /** Private RexBuilder for short-lived expressions. It has its own
-   * dedicated type factory, so don't trust the types to be canonized. */
+  /**
+   * Private RexBuilder for short-lived expressions. It has its own
+   * dedicated type factory, so don't trust the types to be canonized.
+   *
+   * @deprecated Replaced by {@link RexBuilder#DEFAULT}. */
+  @Deprecated // to be removed before 2.0
   final RexBuilder rexBuilder =
       new RexBuilder(new SqlTypeFactoryImpl(RelDataTypeSystemImpl.DEFAULT));
 
@@ -652,6 +656,7 @@ public SqlNode orderField(int ordinal) {
      * @param rex Expression to convert
      */
     public SqlNode toSql(@Nullable RexProgram program, RexNode rex) {
+      rex = dialect.prepareUnparse(rex);
       final RexSubQuery subQuery;
       final SqlNode sqlSubQuery;
       final RexLiteral literal;
@@ -798,7 +803,7 @@ public SqlNode toSql(@Nullable RexProgram program, RexNode rex) {
           //noinspection unchecked
           return toSql(program, search.operands.get(0), literal.getType(), sarg);
         }
-        return toSql(program, RexUtil.expandSearch(implementor().rexBuilder, program, search));
+        return toSql(program, RexUtil.expandSearch(RexBuilder.DEFAULT, program, search));
 
       case EXISTS:
       case UNIQUE:
@@ -953,7 +958,7 @@ private <C extends Comparable<C>> SqlNode toSql(@Nullable RexProgram program,
         final RangeSets.Consumer<C> consumer =
             new RangeToSql<>(operandSql, orList, v ->
                 toSql(program,
-                    implementor().rexBuilder.makeLiteral(v, type)));
+                    RexBuilder.DEFAULT.makeLiteral(v, type)));
         RangeSets.forEach(sarg.rangeSet, consumer);
       }
       return SqlUtil.createCall(SqlStdOperatorTable.OR, POS, orList);
@@ -965,7 +970,7 @@ private <C extends Comparable<C>> SqlNode toIn(SqlNode operandSql,
       final SqlNodeList list = rangeSet.asRanges().stream()
           .map(range ->
               toSql(program,
-                  implementor().rexBuilder.makeLiteral(range.lowerEndpoint(),
+                  RexBuilder.DEFAULT.makeLiteral(range.lowerEndpoint(),
                       type, true, true)))
           .collect(SqlNode.toList());
       switch (list.size()) {
@@ -1718,7 +1723,7 @@ private JoinContext(SqlDialect dialect, Context leftContext,
             && ((RexInputRef) op0).getIndex() >= leftContext.fieldCount) {
           // Arguments were of form 'op1 = op0'
           final SqlOperator op2 = requireNonNull(call.getOperator().reverse());
-          return (RexCall) rexBuilder.makeCall(call.getParserPosition(), op2, op1, op0);
+          return (RexCall) RexBuilder.DEFAULT.makeCall(call.getParserPosition(), op2, op1, op0);
         }
         // fall through
       default:
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index a7d5559c540a..641a0cb35126 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -26,6 +26,7 @@
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
 import org.apache.calcite.runtime.FlatLists;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.SqlCollation;
@@ -42,6 +43,7 @@
 import org.apache.calcite.sql.type.IntervalSqlType;
 import org.apache.calcite.sql.type.MapSqlType;
 import org.apache.calcite.sql.type.MultisetSqlType;
+import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
@@ -94,6 +96,11 @@
  * <p>Some common literal values (NULL, TRUE, FALSE, 0, 1, '') are cached.
  */
 public class RexBuilder {
+
+  /** Default RexBuilder. */
+  public static final RexBuilder DEFAULT =
+      new RexBuilder(new SqlTypeFactoryImpl(RelDataTypeSystemImpl.DEFAULT));
+
   /**
    * Special operator that accesses an unadvertised field of an input record.
    * This operator cannot be used in SQL queries; it is introduced temporarily
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
index e8ed8e7a4915..ef8bd14f40b0 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -795,6 +795,11 @@ public boolean supportBooleanCaseWhen() {
     return true;
   }
 
+  /** Converts {@link RexNode} expression to {@link RexNode} expression before unparse. */
+  public RexNode prepareUnparse(RexNode rexNode) {
+    return rexNode;
+  }
+
   /** Returns whether this dialect supports a given function or operator.
    * It only applies to built-in scalar functions and operators, since
    * user-defined functions and procedures should be read by JdbcSchema. */
diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
index e22b63b13d43..a21c1b577b46 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
@@ -21,6 +21,10 @@
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlAbstractDateTimeLiteral;
 import org.apache.calcite.sql.SqlAlienSystemTypeNameSpec;
 import org.apache.calcite.sql.SqlBasicTypeNameSpec;
@@ -31,11 +35,16 @@
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.fun.SqlFloorFunction;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_FALSE;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_FALSE;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_TRUE;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_TRUE;
 import static org.apache.calcite.util.RelToSqlConverterUtil.unparseHiveTrim;
 
 /**
@@ -93,6 +102,65 @@ public StarRocksSqlDialect(Context context) {
     return true;
   }
 
+  @Override public RexNode prepareUnparse(RexNode arg) {
+    if (!(arg instanceof RexCall)) {
+      return arg;
+    }
+    RexCall call = (RexCall) arg;
+    switch (call.getOperator().getKind()) {
+    case IS_FALSE:
+      RexNode operandIsFalse = call.operands.get(0);
+      if (RexUtil.isDeterministic(operandIsFalse)) {
+        // A IS FALSE -> A IS NOT NULL AND NOT A
+        RexNode isNotNullFunc =
+            RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.IS_NOT_NULL, operandIsFalse);
+        RexNode notFunc =
+            RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.NOT, operandIsFalse);
+        return RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.AND, isNotNullFunc, notFunc);
+      } else {
+        throw new UnsupportedOperationException("Unsupported unparse: "
+            + call.getOperator().getName());
+      }
+    case IS_NOT_FALSE:
+      RexNode operandIsNotFalse = call.operands.get(0);
+      if (RexUtil.isDeterministic(operandIsNotFalse)) {
+        // A IS NOT FALSE -> A IS NULL OR A
+        RexNode isNullFunc =
+            RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.IS_NULL, operandIsNotFalse);
+        return RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.OR, isNullFunc, operandIsNotFalse);
+      } else {
+        throw new UnsupportedOperationException("Unsupported unparse: "
+            + call.getOperator().getName());
+      }
+    case IS_TRUE:
+      RexNode operandIsTrue = call.operands.get(0);
+      if (RexUtil.isDeterministic(operandIsTrue)) {
+        // A IS TRUE -> A IS NOT NULL AND A
+        RexNode isNotNullFunc =
+            RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.IS_NOT_NULL, operandIsTrue);
+        return RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.AND, isNotNullFunc, operandIsTrue);
+      } else {
+        throw new UnsupportedOperationException("Unsupported unparse: "
+            + call.getOperator().getName());
+      }
+    case IS_NOT_TRUE:
+      RexNode operandIsNotTrue = call.operands.get(0);
+      if (RexUtil.isDeterministic(operandIsNotTrue)) {
+        // A IS NOT TRUE -> A IS NULL OR NOT A
+        RexNode isNullFunc =
+            RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.IS_NULL, operandIsNotTrue);
+        RexNode notFunc =
+            RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.NOT, operandIsNotTrue);
+        return RexBuilder.DEFAULT.makeCall(SqlStdOperatorTable.OR, isNullFunc, notFunc);
+      } else {
+        throw new UnsupportedOperationException("Unsupported unparse: "
+            + call.getOperator().getName());
+      }
+    default:
+      return super.prepareUnparse(arg);
+    }
+  }
+
   @Override public void unparseCall(SqlWriter writer, SqlCall call, int leftPrec, int rightPrec) {
     switch (call.getKind()) {
     case ARRAY_VALUE_CONSTRUCTOR:
diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index a0fa34cf3773..c992eaf179f3 100644
--- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -373,7 +373,7 @@ private static String toSql(RelNode root, SqlDialect dialect,
         + "WHERE `product_id` > 0\n"
         + "GROUP BY `product_id`";
     final String expectedStarRocks = "SELECT"
-        + " SUM(CASE WHEN `net_weight` > 0E0 IS TRUE"
+        + " SUM(CASE WHEN `net_weight` > 0E0 IS NOT NULL AND `net_weight` > 0E0"
         + " THEN `shelf_width` ELSE NULL END), SUM(`shelf_width`)\n"
         + "FROM `foodmart`.`product`\n"
         + "WHERE `product_id` > 0\n"
@@ -8492,6 +8492,94 @@ private void checkLiteral2(String expression, String expected) {
         .ok(expected);
   }
 
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-6835">[CALCITE-6835]
+   * Invalid unparse for IS TRUE,IS FALSE,IS NOT TRUE and IS NOT FALSE
+   * in StarRocksDialect</a>. */
+  @Test void testIsTrue() {
+    final String sql = "SELECT * FROM \"EMP\" WHERE \"COMM\" > 0 IS TRUE";
+    final String expected = "SELECT *\n"
+        + "FROM \"SCOTT\".\"EMP\"\n"
+        + "WHERE CAST(\"COMM\" AS DECIMAL(12, 2)) > 0.00 IS TRUE";
+    String expectedStarRocks = "SELECT *\n"
+        + "FROM `SCOTT`.`EMP`\n"
+        + "WHERE CAST(`COMM` AS DECIMAL(12, 2)) > 0.00 IS NOT NULL AND CAST(`COMM` AS DECIMAL(12, 2)) > 0.00";
+    sql(sql)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .ok(expected)
+        .withStarRocks().ok(expectedStarRocks);
+
+    final String sqlNoDeterministic =
+        "SELECT * FROM \"EMP\" WHERE \"COMM\" > RAND_INTEGER(10) IS TRUE";
+    sql(sqlNoDeterministic)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .withStarRocks()
+        .throws_("Unsupported unparse: IS TRUE");
+  }
+
+  @Test void testIsNotTrue() {
+    final String sql = "SELECT * FROM \"EMP\" WHERE \"COMM\" > 0 IS NOT TRUE";
+    final String expected = "SELECT *\n"
+        + "FROM \"SCOTT\".\"EMP\"\n"
+        + "WHERE CAST(\"COMM\" AS DECIMAL(12, 2)) > 0.00 IS NOT TRUE";
+    String expectedStarRocks = "SELECT *\n"
+        + "FROM `SCOTT`.`EMP`\n"
+        + "WHERE CAST(`COMM` AS DECIMAL(12, 2)) > 0.00 IS NULL OR NOT CAST(`COMM` AS DECIMAL(12, 2)) > 0.00";
+    sql(sql)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .ok(expected)
+        .withStarRocks().ok(expectedStarRocks);
+
+    final String sqlNoDeterministic = "SELECT * \n"
+        + "FROM \"EMP\" WHERE \"COMM\" > RAND_INTEGER(10) IS NOT TRUE";
+    sql(sqlNoDeterministic)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .withStarRocks()
+        .throws_("Unsupported unparse: IS NOT TRUE");
+  }
+
+  @Test void testIsFalse() {
+    final String sql = "SELECT * FROM \"EMP\" WHERE \"COMM\" > 0 IS FALSE";
+    final String expected = "SELECT *\n"
+        + "FROM \"SCOTT\".\"EMP\"\n"
+        + "WHERE CAST(\"COMM\" AS DECIMAL(12, 2)) > 0.00 IS FALSE";
+    String expectedStarRocks = "SELECT *\n"
+        + "FROM `SCOTT`.`EMP`\n"
+        + "WHERE CAST(`COMM` AS DECIMAL(12, 2)) > 0.00 IS NOT NULL AND NOT CAST(`COMM` AS DECIMAL(12, 2)) > 0.00";
+    sql(sql)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .ok(expected)
+        .withStarRocks().ok(expectedStarRocks);
+
+    final String sqlNoDeterministic = "SELECT * \n"
+        + "FROM \"EMP\" WHERE \"COMM\" > RAND_INTEGER(10) IS FALSE";
+    sql(sqlNoDeterministic)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .withStarRocks()
+        .throws_("Unsupported unparse: IS FALSE");
+  }
+
+  @Test void testIsNotFalse() {
+    final String sql = "SELECT * FROM \"EMP\" WHERE \"COMM\" > 0 IS NOT FALSE";
+    final String expected = "SELECT *\n"
+        + "FROM \"SCOTT\".\"EMP\"\n"
+        + "WHERE CAST(\"COMM\" AS DECIMAL(12, 2)) > 0.00 IS NOT FALSE";
+    String expectedStarRocks = "SELECT *\n"
+        + "FROM `SCOTT`.`EMP`\n"
+        + "WHERE CAST(`COMM` AS DECIMAL(12, 2)) > 0.00 IS NULL OR CAST(`COMM` AS DECIMAL(12, 2)) > 0.00";
+    sql(sql)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .ok(expected)
+        .withStarRocks().ok(expectedStarRocks);
+
+    final String sqlNoDeterministic = "SELECT * \n"
+        + "FROM \"EMP\" WHERE \"COMM\" > RAND_INTEGER(10) IS NOT FALSE";
+    sql(sqlNoDeterministic)
+        .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
+        .withStarRocks()
+        .throws_("Unsupported unparse: IS NOT FALSE");
+  }
+
   @Test void testMerge() {
     final String sql1 = "merge into \"DEPT\" as \"t\"\n"
         + "using \"DEPT\" as \"s\"\n"


Reviews:
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T02:36:13Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T02:50:55Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T02:52:39Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T03:06:47Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T03:08:04Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:43:07Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:40:50Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:46:03Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:50:05Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:54:02Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:54:30Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T01:10:41Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T01:11:51Z
user: login: suibianwanwank
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T14:39:33Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T22:46:45Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T00:57:52Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T01:29:14Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:22:11Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:26:22Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:43:39Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:58:53Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T01:05:06Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T01:28:31Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T01:59:28Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:15:06Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:20:07Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T02:49:10Z
user: login: mihaibudiu
body: This is actually pretty clean.
I hope other people will like it too.
This hook is very powerful.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T00:27:28Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T00:27:50Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T23:42:12Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T23:46:30Z
user: login: NobiGo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T23:51:12Z

Review comments:
diff_hunk: @@ -128,6 +132,52 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: this won't work if the expression has side effects, e.g., the function is RAND().
created_at: 2025-02-26T02:36:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,52 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: Good catch. I overlooked this non-deterministic expression. If this expression is non-deterministic, I will throw an exception. Or do you have any other more general approaches for handling this situation?
created_at: 2025-02-26T02:50:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,52 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: The only solution I can think of is more complicated: you need to add a new project saving the value of the expression, and then do the expansion for that value.
created_at: 2025-02-26T02:52:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,52 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: Ok, I understand your approach. For this PR, I will throw an exception for non-deterministic expressions. 
created_at: 2025-02-26T03:06:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,52 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: Perhaps we'll consider your suggestion later.
created_at: 2025-02-26T03:08:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -795,6 +795,14 @@ public boolean supportBooleanCaseWhen() {
     return true;
   }
 
+  /**
+   * Returns whether to enable unparse of "macro-like" calls such as IS TRUE.
+   * "A IS TRUE" can be unparse to "A IS NOT NULL AND A" When A is deterministic.
+   * Otherwise, inconsistent results may occur. */
+  public boolean supportMacroLikeUnparse() {
path: core/src/main/java/org/apache/calcite/sql/SqlDialect.java
user: login: mihaibudiu
body: I don't understand how this fixes the problem of non-deterministic functions.
created_at: 2025-03-31T21:39:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: how about having a conservative approximation?
for example, you can have a shuttle which checks whether operand(0) invokes a non-deterministic function.
You can approximate that by "calls any function" for now.
Or the dialect could have a list of non-deterministic function names, there aren't that many.
created_at: 2025-03-31T21:40:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: BTW: I have the impression that there are other places in Calcite where expressions are copied without concern for deterministic behavior.
created_at: 2025-03-31T21:41:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -795,6 +795,14 @@ public boolean supportBooleanCaseWhen() {
     return true;
   }
 
+  /**
+   * Returns whether to enable unparse of "macro-like" calls such as IS TRUE.
+   * "A IS TRUE" can be unparse to "A IS NOT NULL AND A" When A is deterministic.
+   * Otherwise, inconsistent results may occur. */
+  public boolean supportMacroLikeUnparse() {
path: core/src/main/java/org/apache/calcite/sql/SqlDialect.java
user: login: NobiGo
body: This has not been fixed. It would be more accurate to say that it is a kind of compatible approach, which refers to SqlValidator#withCallRewrite.
created_at: 2025-04-01T00:40:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -795,6 +795,14 @@ public boolean supportBooleanCaseWhen() {
     return true;
   }
 
+  /**
+   * Returns whether to enable unparse of "macro-like" calls such as IS TRUE.
+   * "A IS TRUE" can be unparse to "A IS NOT NULL AND A" When A is deterministic.
+   * Otherwise, inconsistent results may occur. */
+  public boolean supportMacroLikeUnparse() {
path: core/src/main/java/org/apache/calcite/sql/SqlDialect.java
user: login: mihaibudiu
body: I think this is not really necessary then. 
created_at: 2025-04-01T00:46:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: My current approach is indeed a bit strange. Since this might not be within the purview of SqlDialect, in StarRocksSqlDialect, it is not certain whether this function is deterministic(SqlCall). If we make this judgment in SqlImplementor, we need a list of unsupported functions to determine whether to rewrite it. This list is used to write functions that Dialect does not support and which might be non-deterministic. WDYT?
created_at: 2025-04-01T00:50:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -795,6 +795,14 @@ public boolean supportBooleanCaseWhen() {
     return true;
   }
 
+  /**
+   * Returns whether to enable unparse of "macro-like" calls such as IS TRUE.
+   * "A IS TRUE" can be unparse to "A IS NOT NULL AND A" When A is deterministic.
+   * Otherwise, inconsistent results may occur. */
+  public boolean supportMacroLikeUnparse() {
path: core/src/main/java/org/apache/calcite/sql/SqlDialect.java
user: login: NobiGo
body: Ok. This approach is somewhat inappropriate.
created_at: 2025-04-01T00:54:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: This is probably not the only place where a rewrite could be wrong if a call is not deterministic. I think it would be a useful feature to be able to detect such calls. 
created_at: 2025-04-01T00:54:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: We already have RexUtil#isDeterministic for detecting whether a function is deterministic. Currently, what I'm thinking is that only when there is a transformation similar to "IS TRUE", where a reference becomes multiple times, will it lead to inconsistent results. Functions like COALESCE and NULLIF fall into this category.
created_at: 2025-04-01T01:10:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: Right 
created_at: 2025-04-01T01:11:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: suibianwanwank
body: I agree, any scenario requiring splitting a rexNode into multiple processing paths would encounter this problem. But it seems difficult to resolve at the root cause - the only solution I can think of is to specially handle it by adding an additional Project whenever this situation occurs.  
created_at: 2025-04-01T14:39:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,6 +132,72 @@ public StarRocksSqlDialect(Context context) {
               timeUnitNode.getParserPosition());
       SqlFloorFunction.unparseDatetimeFunction(writer, newCall, "DATE_TRUNC", false);
       break;
+    case IS_TRUE:
+      if (this.supportMacroLikeUnparse()) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: I've revised the PR according to the feedback. When you have time, please review the updates.
created_at: 2025-04-01T22:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,6 +94,18 @@ public StarRocksSqlDialect(Context context) {
     return true;
   }
 
+  @Override public boolean supportsUnparseOnlyDeterministic(SqlOperator sqlOperator) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: mihaibudiu
body: I am not sure this function is useful.
If the function returns 'false' and you cannot unparse, you will need to throw.
If you remove the function, you still throw in the same places.
created_at: 2025-04-02T00:57:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,6 +94,18 @@ public StarRocksSqlDialect(Context context) {
     return true;
   }
 
+  @Override public boolean supportsUnparseOnlyDeterministic(SqlOperator sqlOperator) {
path: core/src/main/java/org/apache/calcite/sql/dialect/StarRocksSqlDialect.java
user: login: NobiGo
body: If we do not support rewriting this function, we should throw exceptions at the point of rewriting. We directly convert it because only Starrocks has been identified to have this issue. If it is discovered later that operations like A IS TRUE and A IS NOT NULL are also unsupported, exceptions must be thrown during the conversion process. This conversion logic can be implemented as part of the Dialect behavior.
If I remove this function, the A IS FALSE will remain unchanged and not be converted. 
What I'm thinking here is that not all dialects have undergone such transformation. If the database supports A IS TRUE, then we won't make any changes.
created_at: 2025-04-02T01:29:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: Can this be done in unparseCall instead?
Then you don't need the new policy function.
created_at: 2025-04-02T17:22:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: If we use unparseCall, we need to parse the SqlCall. We can't extract whether it is deterministic from SqlCall. So, I put it here.
created_at: 2025-04-03T00:26:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: true.
I don't really like this function "supportsUnparseOnlyDeterministic()", because it really has nothing to do with IS_TRUE, etc. But I don't have a better proposal on how to structure this.
created_at: 2025-04-03T00:43:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: I have also been pondering over this for a long time. How can I accurately express it so that the developers can quickly figure out which functions should be added here and which ones should use unparseCall instead?
How about `unparseCallPartial` The meaning is that this function "unparse" is supported in certain scenarios, not in all cases.
created_at: 2025-04-03T00:58:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: The most generic way to handle this would be to add a hook in the dialect to rewrite calls before they are converted to sql. This hook would be called from callToSql prior to converting to SqlNode.

created_at: 2025-04-03T01:05:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: I understand. I will try to do it this way.
created_at: 2025-04-03T01:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: the only misgivings I have about that is that the Dialect class is mostly about SqlNode.
But it does have a method that takes a RexNode
created_at: 2025-04-03T01:59:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: Adding RexNode parsing to Dialect might render the current unparseCall ineffective. After all, RexNode offers greater flexibility.
created_at: 2025-04-03T02:15:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: Or we could change "supportsUnparseOnlyDeterministic" in the current PR to "supportsUnparsePartial" and mark it as @Experimental. Then we'll see if it's necessary to add the parsing of RexNode to Dialect later.
created_at: 2025-04-03T02:20:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -855,6 +855,76 @@ private SqlNode callToSql(@Nullable RexProgram program, RexCall call0,
       final RexCall call = (RexCall) stripCastFromString(call1, dialect);
       SqlOperator op = call.getOperator();
       switch (op.getKind()) {
+      case IS_FALSE:
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: I am thinking of a function in Dialect like this: `RexCall prepareUnparse(RexCall arg)` which can rewrite a RexCall before it is unparsed; the default implementation returns arg. The argument could be RexNode too.
created_at: 2025-04-03T02:49:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,8 +166,12 @@ public abstract class SqlImplementor {
 
   protected final Map<CorrelationId, Context> correlTableMap = new HashMap<>();
 
-  /** Private RexBuilder for short-lived expressions. It has its own
-   * dedicated type factory, so don't trust the types to be canonized. */
+  /**
+   * Private RexBuilder for short-lived expressions. It has its own
+   * dedicated type factory, so don't trust the types to be canonized.
+   *
+   * @deprecated Replaced by {@link RexBuilder#DEFAULT}. */
+  @Deprecated // to be removed before 2.0
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: I am not sure about this change, though
created_at: 2025-04-04T00:27:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,8 +166,12 @@ public abstract class SqlImplementor {
 
   protected final Map<CorrelationId, Context> correlTableMap = new HashMap<>();
 
-  /** Private RexBuilder for short-lived expressions. It has its own
-   * dedicated type factory, so don't trust the types to be canonized. */
+  /**
+   * Private RexBuilder for short-lived expressions. It has its own
+   * dedicated type factory, so don't trust the types to be canonized.
+   *
+   * @deprecated Replaced by {@link RexBuilder#DEFAULT}. */
+  @Deprecated // to be removed before 2.0
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: This change is necessary. I tried to obtain `RexBuilder` through `implementer().RexBuilder`, but SimpleContext may throw UnsupportedOperaException in some cases.
created_at: 2025-04-04T23:42:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,8 +166,12 @@ public abstract class SqlImplementor {
 
   protected final Map<CorrelationId, Context> correlTableMap = new HashMap<>();
 
-  /** Private RexBuilder for short-lived expressions. It has its own
-   * dedicated type factory, so don't trust the types to be canonized. */
+  /**
+   * Private RexBuilder for short-lived expressions. It has its own
+   * dedicated type factory, so don't trust the types to be canonized.
+   *
+   * @deprecated Replaced by {@link RexBuilder#DEFAULT}. */
+  @Deprecated // to be removed before 2.0
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: mihaibudiu
body: Well, I approved the PR.
But it would be good to know why this method needs to be essentially deprecated (because it can't be used).

created_at: 2025-04-04T23:46:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -166,8 +166,12 @@ public abstract class SqlImplementor {
 
   protected final Map<CorrelationId, Context> correlTableMap = new HashMap<>();
 
-  /** Private RexBuilder for short-lived expressions. It has its own
-   * dedicated type factory, so don't trust the types to be canonized. */
+  /**
+   * Private RexBuilder for short-lived expressions. It has its own
+   * dedicated type factory, so don't trust the types to be canonized.
+   *
+   * @deprecated Replaced by {@link RexBuilder#DEFAULT}. */
+  @Deprecated // to be removed before 2.0
path: core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
user: login: NobiGo
body: I am concerned that other projects have implemented this interface and obtained this RexBuilder instance through `implementer(). RexBuilder`.We don't need it anymore. To maintain compatibility, I added @ Deprecated.
created_at: 2025-04-04T23:51:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-28T03:41:19Z
body: This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 90 days if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@calcite.apache.org list. Thank you for your contributions.
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-07T01:24:44Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=apache_calcite&pullRequest=4214) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12 New issues](https://sonarcloud.io/project/issues?id=apache_calcite&pullRequest=4214&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=apache_calcite&pullRequest=4214&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=apache_calcite&pullRequest=4214&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=apache_calcite&pullRequest=4214&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=apache_calcite&pullRequest=4214&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=apache_calcite&pullRequest=4214)


author_association: NONE