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)");