From a0d4a6b81a1d5762f36e584763a793a9fcc12a8f Mon Sep 17 00:00:00 2001 From: Dwrite Date: Sun, 30 Aug 2026 16:56:21 +0800 Subject: [PATCH] [CALCITE-7749] CONCAT (and other STRING_SAME_SAME-typed operators) gives misleading error message for mixed CHARACTER/BINARY arguments --- .../apache/calcite/sql/type/OperandTypes.java | 82 ++++++++++++++++++- .../apache/calcite/test/SqlValidatorTest.java | 7 +- .../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 114b11cf169..3461d07cebb 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 @@ -1134,14 +1134,16 @@ public static SqlSingleOperandTypeChecker same(int operandCount, * same string type family. */ public static final SqlSingleOperandTypeChecker STRING_SAME_SAME = - STRING_STRING.and(SAME_SAME); + withSignatureGenerator(STRING_STRING.and(SAME_SAME), + stringSameSameSignatureGenerator(2)); /** * 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), + stringSameSameSignatureGenerator(3)); public static final SqlSingleOperandTypeChecker STRING_STRING_INTEGER = family(SqlTypeFamily.STRING, SqlTypeFamily.STRING, SqlTypeFamily.INTEGER); @@ -1192,8 +1194,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), + stringSameSameSignatureGenerator(2, 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 +1501,78 @@ private RecordTypeWithOneFieldChecker(Predicate predicate) { return !validationError; } } + /** + * Wraps a {@link SqlSingleOperandTypeChecker}, overriding only its + * {@link SqlOperandTypeChecker#getAllowedSignatures}. Needed because + * {@link CompositeOperandTypeChecker#withGenerator} always returns a plain + * {@link CompositeOperandTypeChecker}, which loses single-operand checking + * (see CALCITE-7749). + */ + private static SqlSingleOperandTypeChecker withSignatureGenerator( + SqlSingleOperandTypeChecker checker, + BiFunction 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 a signature generator for STRING_SAME_SAME-style checkers, which + * are composed of a family check (STRING) AND'd with a same-family check. + * The default composite signature only shows the STRING half and drops the + * "same concrete sub-family" constraint (see CALCITE-7749), so this + * generator instead lists both concretely valid forms: all-CHARACTER and + * all-BINARY. + */ + private static BiFunction stringSameSameSignatureGenerator( + int stringOperandCount, SqlTypeFamily... trailingFamilies) { + return (op, opName) -> { + List charForm = new ArrayList<>(); + List binaryForm = new ArrayList<>(); + for (int i = 0; i < stringOperandCount; i++) { + charForm.add(SqlTypeFamily.CHARACTER.name()); + binaryForm.add(SqlTypeFamily.BINARY.name()); + } + for (SqlTypeFamily family : trailingFamilies) { + charForm.add(family.name()); + binaryForm.add(family.name()); + } + return SqlUtil.getAliasedSignature(op, opName, charForm) + + SqlOperator.NL + + SqlUtil.getAliasedSignature(op, opName, binaryForm); + }; + } /** 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 6390c8ca7e0..f8bde611318 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 " - + "' \\|\\| '.*Supported form.s.: " - + "' \\|\\| .*'"); + + "' \\|\\| '.*" + + "Supported form\\(s\\): " + + "' \\|\\| '\\s*" + + "' \\|\\| '\\s*" + + "' \\|\\| '.*"); } /** 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 6082c27638d..43568d28cb9 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -2757,7 +2757,8 @@ private static void checkConcat2Func(SqlOperatorFixture f) { f.checkFails("^concat('a', x'0a')^", "Cannot apply 'CONCAT' to arguments of type " + "'CONCAT\\(, \\)'\\. Supported " - + "form\\(s\\): 'CONCAT\\(, \\)'", + + "form\\(s\\): 'CONCAT\\(, \\)'\n" + + "'CONCAT\\(, \\)'", false); } @@ -11873,7 +11874,8 @@ void checkStartsWith(SqlOperatorFixture f0, FunctionAlias functionAlias) { f.checkFails("^" + fn + "('aabbcc', x'aa')^", "Cannot apply '" + fn + "' to arguments of type " + "'" + fn + "\\(, \\)'\\. Supported " - + "form\\(s\\): '" + fn + "\\(, \\)'", + + "form\\(s\\): '" + fn + "\\(, \\)'\\n" + + "'" + fn + "\\(, \\)'", false); f.checkNull(fn + "(null, null)"); f.checkNull(fn + "('12345', null)"); @@ -11913,7 +11915,8 @@ void checkEndsWith(SqlOperatorFixture f0, FunctionAlias functionAlias) { f.checkFails("^" + fn + "('aabbcc', x'aa')^", "Cannot apply '" + fn + "' to arguments of type " + "'" + fn + "\\(, \\)'\\. Supported " - + "form\\(s\\): '" + fn + "\\(, \\)'", + + "form\\(s\\): '" + fn + "\\(, \\)'\\s*" + + "'" + fn + "\\(, \\)'", false); f.checkNull(fn + "(null, null)"); f.checkNull(fn + "('12345', null)");