From 9398704f7ea4d0dc525dc8ba0bbc026eb12f443c Mon Sep 17 00:00:00 2001 From: Mihai Budiu Date: Wed, 14 Jan 2026 21:47:50 -0800 Subject: [PATCH] [CALCITE-7360] The meaning of negation for unsigned numbers is not defined Signed-off-by: Mihai Budiu --- .../calcite/sql/fun/SqlStdOperatorTable.java | 2 +- .../apache/calcite/sql/type/OperandTypes.java | 65 ++++++++++++++++++- core/src/test/resources/sql/unsigned.iq | 5 ++ .../apache/calcite/test/SqlOperatorTest.java | 22 +++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java index 2a26a78929cf..1137539fdc76 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java @@ -1023,7 +1023,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { 80, ReturnTypes.ARG0, InferTypes.RETURN_TYPE, - OperandTypes.NUMERIC_OR_INTERVAL); + OperandTypes.SIGNED_OR_INTERVAL); /** * Checked version of prefix arithmetic minus operator, '-'. 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 b67899d761a0..fb7d762785a5 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 @@ -402,10 +402,70 @@ public static SqlOperandTypeChecker variadic( public static final SqlSingleOperandTypeChecker INTEGER = family(SqlTypeFamily.INTEGER); + /** Operand type checker that only allows signed types. + * This is almost like an OR of 4 type families (INTEGER, APPROXIMATE_NUMERIC, DECIMAL) + * but OR allows implicit casts to any of the types, and this checker doesn't. */ + public static final SqlSingleOperandTypeChecker SIGNED = new SqlSingleOperandTypeChecker() { + @Override public boolean checkSingleOperandType(SqlCallBinding callBinding, SqlNode operand, + int iFormalOperand, boolean throwOnFailure) { + RelDataType type = SqlTypeUtil.deriveType(callBinding, operand); + SqlTypeName typeName = type.getSqlTypeName(); + boolean isLegal = SqlTypeName.INT_TYPES.contains(typeName) + || SqlTypeName.APPROX_TYPES.contains(typeName) + || typeName == SqlTypeName.DECIMAL; + + if (!isLegal) { + if (throwOnFailure) { + throw callBinding.newValidationSignatureError(); + } + return false; + } + return true; + } + + @Override public boolean checkOperandTypes( + SqlCallBinding callBinding, + boolean throwOnFailure) { + // This is a specialized implementation of FamilyOperandTypeChecker.checkOperandTypes. + SqlNode op = callBinding.operands().get(0); + if (!checkSingleOperandType(callBinding, op, 0, false)) { + // try to coerce type if it is allowed. + boolean coerced = false; + if (callBinding.isTypeCoercionEnabled()) { + // Also allow expressions that can be coerced to NUMERIC (e.g. type CHAR) + TypeCoercion typeCoercion = callBinding.getValidator().getTypeCoercion(); + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(callBinding.getOperandType(0)); + ImmutableList dataTypes = builder.build(); + coerced = + typeCoercion.builtinFunctionCoercion( + callBinding, dataTypes, ImmutableList.of(SqlTypeFamily.NUMERIC)); + } + // re-validate the new nodes type. + SqlNode op1 = callBinding.operands().get(0); + if (!checkSingleOperandType( + callBinding, + op1, + 0, + throwOnFailure)) { + return false; + } + return coerced; + } + return true; + } + + @Override public String getAllowedSignatures(SqlOperator op, String opName) { + return SqlUtil.getAliasedSignature(op, opName, ImmutableList.of(SqlTypeFamily.INTEGER)) + "\n" + + SqlUtil.getAliasedSignature( + op, opName, ImmutableList.of(SqlTypeFamily.APPROXIMATE_NUMERIC)) + "\n" + + SqlUtil.getAliasedSignature(op, opName, ImmutableList.of(SqlTypeFamily.DECIMAL)); + } + }; + public static final SqlSingleOperandTypeChecker UNSIGNED_NUMERIC_UNSIGNED_NUMERIC = family(SqlTypeFamily.UNSIGNED_NUMERIC, SqlTypeFamily.UNSIGNED_NUMERIC); - public static final SqlSingleOperandTypeChecker INTEGER_INTEGER = family(SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER); @@ -1297,6 +1357,9 @@ public static SqlSingleOperandTypeChecker same(int operandCount, public static final SqlSingleOperandTypeChecker NUMERIC_OR_INTERVAL = NUMERIC.or(INTERVAL); + public static final SqlSingleOperandTypeChecker SIGNED_OR_INTERVAL = + SIGNED.or(INTERVAL); + public static final SqlSingleOperandTypeChecker NUMERIC_OR_STRING = NUMERIC.or(STRING); diff --git a/core/src/test/resources/sql/unsigned.iq b/core/src/test/resources/sql/unsigned.iq index f496dbb2209c..83112fe87edf 100644 --- a/core/src/test/resources/sql/unsigned.iq +++ b/core/src/test/resources/sql/unsigned.iq @@ -24,6 +24,11 @@ EXPR$0 6 !ok +SELECT -CAST(200 AS INT UNSIGNED); +java.sql.SQLException: Error while executing SQL "SELECT -CAST(200 AS INT UNSIGNED)": From line 1, column 8 to line 1, column 33: Cannot apply '-' to arguments of type '-'. Supported form(s): '-' + +!error + SELECT CAST(200 AS INT UNSIGNED) - 100; EXPR$0 100 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 c7144c92fbdc..a57a45bcee56 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -16747,6 +16747,28 @@ private static void checkLogicalOrFunc(SqlOperatorFixture f) { f.checkNull("CAST(NULL AS INTEGER UNSIGNED) ^^ CAST(NULL AS INTEGER UNSIGNED)"); } + @Test void testUnsignedArithmetic() { + final SqlOperatorFixture f = fixture(); + // Test case for [CALCITE-7360] The meaning of negation for unsigned numbers is not defined + f.checkFails("^-CAST (100 AS INT UNSIGNED)^", + "Cannot apply '-' to arguments of type '-'\\. " + + "Supported form\\(s\\): '-'\\n" + + "'-'\\n" + + "'-'\\n" + + "'-'", false); + f.checkScalar("CAST(2 AS INT UNSIGNED)", "2", "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) + CAST(2 AS INT UNSIGNED)", "4", + "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) + CAST(2 AS TINYINT UNSIGNED)", "4", + "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) + 2", "4", "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) - 2", "0", "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) - CAST(2 AS TINYINT UNSIGNED)", "0", + "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) * 2", "4", "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2 AS INT UNSIGNED) / 2", "1", "INTEGER UNSIGNED NOT NULL"); + } + /** * Test cases for * [CALCITE-7109]