This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 7359405d87 [CALCITE-7749] CONCAT (and other STRING_SAME_SAME-typed 
operators) gives misleading error message for mixed CHARACTER/BINARY arguments
7359405d87 is described below

commit 7359405d8794f5198cc4188c13aaea0f3dd13402
Author: Dwrite <[email protected]>
AuthorDate: Sun Aug 30 16:56:21 2026 +0800

    [CALCITE-7749] CONCAT (and other STRING_SAME_SAME-typed operators) gives 
misleading error message for mixed CHARACTER/BINARY arguments
---
 .../org/apache/calcite/sql/type/OperandTypes.java  | 82 ++++++++++++++++++++--
 .../org/apache/calcite/test/SqlValidatorTest.java  |  7 +-
 .../org/apache/calcite/test/SqlOperatorTest.java   |  9 ++-
 3 files changed, 89 insertions(+), 9 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index 114b11cf16..e36017ad30 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -57,6 +57,7 @@
 import java.util.function.Function;
 import java.util.function.IntFunction;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -1133,15 +1134,19 @@ public static SqlSingleOperandTypeChecker same(int 
operandCount,
    * Operand type-checking strategy where two operands must both be in the
    * same string type family.
    */
+  private static final ImmutableList<SqlTypeFamily> STRING_SUB_FAMILIES =
+      ImmutableList.of(SqlTypeFamily.CHARACTER, SqlTypeFamily.BINARY);
   public static final SqlSingleOperandTypeChecker STRING_SAME_SAME =
-      STRING_STRING.and(SAME_SAME);
+      withSignatureGenerator(STRING_STRING.and(SAME_SAME),
+          sameSubFamilySignatureGenerator(2, STRING_SUB_FAMILIES));
 
   /**
    * Operand type-checking strategy where three operands must all be in the
    * same string type family.
    */
   public static final SqlSingleOperandTypeChecker STRING_SAME_SAME_SAME =
-      STRING_STRING_STRING.and(SAME_SAME_SAME);
+      withSignatureGenerator(STRING_STRING_STRING.and(SAME_SAME_SAME),
+          sameSubFamilySignatureGenerator(3, STRING_SUB_FAMILIES));
 
   public static final SqlSingleOperandTypeChecker STRING_STRING_INTEGER =
       family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, 
SqlTypeFamily.INTEGER);
@@ -1192,8 +1197,8 @@ public static SqlSingleOperandTypeChecker same(int 
operandCount,
    * same string type family and last type is INTEGER.
    */
   public static final SqlSingleOperandTypeChecker STRING_SAME_SAME_INTEGER =
-      STRING_STRING_INTEGER.and(SAME_SAME_INTEGER);
-
+      withSignatureGenerator(STRING_STRING_INTEGER.and(SAME_SAME_INTEGER),
+          sameSubFamilySignatureGenerator(2, STRING_SUB_FAMILIES, 
SqlTypeFamily.INTEGER));
   public static final SqlSingleOperandTypeChecker 
STRING_SAME_SAME_OR_ARRAY_SAME_SAME =
       or(STRING_SAME_SAME,
           and(OperandTypes.SAME_SAME, family(SqlTypeFamily.ARRAY, 
SqlTypeFamily.ARRAY)));
@@ -1499,6 +1504,75 @@ private 
RecordTypeWithOneFieldChecker(Predicate<SqlTypeName> predicate) {
       return !validationError;
     }
   }
+  /**
+   * Wraps a {@link SqlSingleOperandTypeChecker}, overriding only its
+   * {@link SqlOperandTypeChecker#getAllowedSignatures}, while delegating
+   * everything else unchanged. Used instead of
+   * {@link CompositeOperandTypeChecker#withGenerator}, which always returns
+   * a plain {@link CompositeOperandTypeChecker} and would lose single-operand
+   * checking.
+   */
+  private static SqlSingleOperandTypeChecker withSignatureGenerator(
+      SqlSingleOperandTypeChecker checker,
+      BiFunction<SqlOperator, String, String> signatureGenerator) {
+    return new SqlSingleOperandTypeChecker() {
+      @Override public boolean checkSingleOperandType(SqlCallBinding 
callBinding,
+          SqlNode operand, int iFormalOperand, boolean throwOnFailure) {
+        // STRING_SAME_SAME-style checkers evaluate family membership AND
+        // cross-operand comparability together; there's no meaningful way to
+        // validate a single operand in isolation, so fall back to the full
+        // multi-operand check.
+        return checker.checkOperandTypes(callBinding, throwOnFailure);
+      }
+
+      @Override public boolean checkOperandTypes(SqlCallBinding callBinding,
+          boolean throwOnFailure) {
+        // Explicitly override rather than relying on the interface default
+        // (which would route through checkSingleOperandType(operand(0), 0)
+        // and only ever check the first operand).
+        return checker.checkOperandTypes(callBinding, throwOnFailure);
+      }
+
+      @Override public SqlOperandCountRange getOperandCountRange() {
+        return checker.getOperandCountRange();
+      }
+
+      @Override public boolean isOptional(int i) {
+        return checker.isOptional(i);
+      }
+
+      @Override public Consistency getConsistency() {
+        return checker.getConsistency();
+      }
+
+      @Override public String getAllowedSignatures(SqlOperator op, String 
opName) {
+        return signatureGenerator.apply(op, opName);
+      }
+    };
+  }
+
+  /**
+   * Builds an error-message generator that lists one candidate signature per
+   * given sub-family, each repeated across the "same" operand positions,
+   * followed by any trailing fixed families (see CALCITE-7749).
+   */
+  private static BiFunction<SqlOperator, String, String> 
sameSubFamilySignatureGenerator(
+      int sameOperandCount, List<SqlTypeFamily> subFamilies,
+      SqlTypeFamily... trailingFamilies) {
+    return (op, opName) ->
+        subFamilies.stream()
+            .map(subFamily -> {
+              List<String> form = new ArrayList<>();
+              for (int i = 0; i < sameOperandCount; i++) {
+                form.add(subFamily.name());
+              }
+              for (SqlTypeFamily family : trailingFamilies) {
+                form.add(family.name());
+              }
+              return SqlUtil.getAliasedSignature(op, opName, form);
+            })
+            .collect(Collectors.joining(SqlOperator.NL));
+  }
 
   /** Checker that returns whether a value is a collection (multiset or array)
    * of scalar or record values. */
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index f5d9f76dfe..3915e5723c 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -872,8 +872,11 @@ static SqlOperatorTable operatorTableFor(SqlLibrary 
library) {
   @Test void testConcatFails() {
     wholeExpr("'a'||x'ff'")
         .fails("(?s).*Cannot apply '\\|\\|' to arguments of type "
-            + "'<CHAR.1.> \\|\\| <BINARY.1.>'.*Supported form.s.: "
-            + "'<STRING> \\|\\| <STRING>.*'");
+            + "'<CHAR\\(1\\)> \\|\\| <BINARY\\(1\\)>'.*"
+            + "Supported form\\(s\\): "
+            + "'<CHARACTER> \\|\\| <CHARACTER>'\\s*"
+            + "'<BINARY> \\|\\| <BINARY>'\\s*"
+            + "'<EQUIVALENT_TYPE> \\|\\| <EQUIVALENT_TYPE>'.*");
   }
 
   /** Tests the CONCAT function, which unlike the concat operator ('||') is not
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 504308f745..e584e59c37 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -2791,7 +2791,8 @@ private static void checkConcat2Func(SqlOperatorFixture 
f) {
     f.checkFails("^concat('a', x'0a')^",
         "Cannot apply 'CONCAT' to arguments of type "
             + "'CONCAT\\(<CHAR\\(1\\)>, <BINARY\\(1\\)>\\)'\\. Supported "
-            + "form\\(s\\): 'CONCAT\\(<STRING>, <STRING>\\)'",
+            + "form\\(s\\): 'CONCAT\\(<CHARACTER>, <CHARACTER>\\)'\n"
+            + "'CONCAT\\(<BINARY>, <BINARY>\\)'",
         false);
   }
 
@@ -11914,7 +11915,8 @@ void checkStartsWith(SqlOperatorFixture f0, 
FunctionAlias functionAlias) {
       f.checkFails("^" + fn + "('aabbcc', x'aa')^",
           "Cannot apply '" + fn + "' to arguments of type "
               + "'" + fn + "\\(<CHAR\\(6\\)>, <BINARY\\(1\\)>\\)'\\. Supported 
"
-              + "form\\(s\\): '" + fn + "\\(<STRING>, <STRING>\\)'",
+              + "form\\(s\\): '" + fn + "\\(<CHARACTER>, <CHARACTER>\\)'\\n"
+              + "'" + fn + "\\(<BINARY>, <BINARY>\\)'",
           false);
       f.checkNull(fn + "(null, null)");
       f.checkNull(fn + "('12345', null)");
@@ -11954,7 +11956,8 @@ void checkEndsWith(SqlOperatorFixture f0, FunctionAlias 
functionAlias) {
       f.checkFails("^" + fn + "('aabbcc', x'aa')^",
           "Cannot apply '" + fn + "' to arguments of type "
               + "'" + fn + "\\(<CHAR\\(6\\)>, <BINARY\\(1\\)>\\)'\\. Supported 
"
-              + "form\\(s\\): '" + fn + "\\(<STRING>, <STRING>\\)'",
+              + "form\\(s\\): '" + fn + "\\(<CHARACTER>, <CHARACTER>\\)'\\s*"
+              + "'" + fn + "\\(<BINARY>, <BINARY>\\)'",
           false);
       f.checkNull(fn + "(null, null)");
       f.checkNull(fn + "('12345', null)");

Reply via email to