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]