diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index c5d392e2fb0c..b9c295211c9d 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -279,6 +279,24 @@ public class ${parser.class} extends SqlAbstractParserImpl Span.of(table, extendList).pos(), table, extendList); } + /** + * Returns the parser position of the given token. + * + *

Declared as a plain method (rather than JAVACODE) so it can be called + * from generated lookahead code, which does not declare + * {@code throws ParseException}. + * + * @param token token whose position to return + * @return parser position spanning the given token + */ + private static SqlParserPos pos(Token token) { + return new SqlParserPos( + token.beginLine, + token.beginColumn, + token.endLine, + token.endColumn); + } + /** Adds a warning that a token such as "HOURS" was used, * whereas the SQL standard only allows "HOUR". * @@ -8470,6 +8488,17 @@ SqlBinaryOperator BinaryRowOperator() : // is handled as a special case { return SqlStdOperatorTable.EQUALS; } | { return SqlStdOperatorTable.BIT_LEFT_SHIFT; } + // The right shift operator ">>" is matched as two ">" tokens rather than a + // single ">>" token. A greedy ">>" token would be produced by the lexer even + // when the two ">" characters close nested angle-bracket types (e.g. + // MAP>), breaking type parsing. Matching two ">" tokens here + // keeps right shift confined to expression context. The semantic lookahead + // requires the two ">" to be immediately adjacent (no intervening whitespace), + // so "a > > b" is not treated as a right shift; otherwise we fall through to + // the single ">" (greater-than) alternative below. +| LOOKAHEAD({ getToken(1).kind == GT && getToken(2).kind == GT + && pos(getToken(1)).endsImmediatelyBefore(pos(getToken(2))) }) + { return SqlStdOperatorTable.BIT_RIGHT_SHIFT; } | { return SqlStdOperatorTable.GREATER_THAN; } | { return SqlStdOperatorTable.LESS_THAN; } | { return SqlStdOperatorTable.LESS_THAN_OR_EQUAL; } diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index acbd66cb1cc7..57edf126a5c4 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -377,6 +377,7 @@ import static org.apache.calcite.sql.fun.SqlStdOperatorTable.BIT_AND; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.BIT_LEFT_SHIFT; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.BIT_OR; +import static org.apache.calcite.sql.fun.SqlStdOperatorTable.BIT_RIGHT_SHIFT; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.BIT_XOR; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CARDINALITY; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CAST; @@ -512,6 +513,7 @@ import static org.apache.calcite.sql.fun.SqlStdOperatorTable.REGR_COUNT; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.REINTERPRET; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.REPLACE; +import static org.apache.calcite.sql.fun.SqlStdOperatorTable.RIGHTSHIFT; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ROUND; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ROW; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ROW_NUMBER; @@ -920,6 +922,18 @@ void populate1() { // (e.g., x << y) defineMethod(BIT_LEFT_SHIFT, BuiltInMethod.LEFT_SHIFT.method, NullPolicy.STRICT); + // Right shift operations: shift bits to the right by specified amount. + // Supports integer and unsigned integer data types. Binary right shift is + // intentionally not supported; see [CALCITE-7651]. + // Shift amount is normalized using modulo arithmetic based on data type bit width. + + // RIGHTSHIFT: Function call syntax for bitwise right shift operation (e.g., RIGHTSHIFT(x, y)) + defineMethod(RIGHTSHIFT, BuiltInMethod.RIGHT_SHIFT.method, NullPolicy.STRICT); + + // BIT_RIGHT_SHIFT: Operator syntax for bitwise right shift in SQL expressions + // (e.g., x >> y) + defineMethod(BIT_RIGHT_SHIFT, BuiltInMethod.RIGHT_SHIFT.method, NullPolicy.STRICT); + define(SAFE_ADD, new SafeArithmeticImplementor(BuiltInMethod.SAFE_ADD.method)); define(SAFE_DIVIDE, diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 789fb9d6c794..41fc4484b85d 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -3772,6 +3772,21 @@ private static ByteString binaryOperator( return new ByteString(result); } + /** + * Returns {@code x} modulo {@code m}, normalized to the range {@code [0, m)} + * (unlike {@code %}, the result is never negative). Used to normalize a shift + * amount to the bit width of the value being shifted. + * + * @param x the value (typically a shift amount, which may be negative) + * @param m the modulus, which must be positive (typically a bit width) + * @return {@code x} modulo {@code m}, in the range {@code [0, m)} + */ + private static int positiveModulo(long x, int m) { + // Math.floorMod(long, int) is only available since JDK 9, so widen to + // Math.floorMod(long, long) and narrow the result (always in [0, m)) to int. + return (int) Math.floorMod(x, (long) m); + } + /** * Performs PostgresSQL-style bitwise shift on a 32-bit integer. * @@ -3779,8 +3794,8 @@ private static ByteString binaryOperator( * @param y the shift amount (positive: left shift, negative: right shift) * @return the shifted integer */ - public static int leftShift(int x, int y) { - int shift = ((y % 32) + 32) % 32; // normalize to 0~31 + public static int leftShift(int x, long y) { + int shift = positiveModulo(y, 32); // normalize to 0~31 return y >= 0 ? x << shift : x >> shift; // arithmetic right shift } @@ -3793,23 +3808,11 @@ public static int leftShift(int x, int y) { * @param y the shift amount * @return the shifted long value */ - public static long leftShift(long x, int y) { - int shift = ((y % 64) + 64) % 64; // normalize to 0~63 + public static long leftShift(long x, long y) { + int shift = positiveModulo(y, 64); // normalize to 0~63 return y >= 0 ? x << shift : x >> shift; } - /** - * Performs PostgresSQL-style bitwise shift on an int value with a long shift amount. - * - * @param x the int value to shift - * @param y the long shift amount - * @return the shifted value as long - */ - public static long leftShift(int x, long y) { - int shift = (int) (((y % 32) + 32) % 32); // normalize to 0~31 - return y >= 0 ? (long) x << shift : (long) x >> shift; - } - /** * Performs PostgresSQL-style bitwise shift on a byte array. * Positive shift: left shift. @@ -3819,7 +3822,7 @@ public static long leftShift(int x, long y) { * @param y the shift amount in bits * @return the shifted byte array */ - public static byte[] leftShift(byte[] bytes, int y) { + public static byte[] leftShift(byte[] bytes, long y) { if (bytes.length == 0) { return new byte[0]; } @@ -3828,7 +3831,7 @@ public static byte[] leftShift(byte[] bytes, int y) { // PostgreSQL behavior: always treat as left shift with modulo arithmetic // Negative y becomes equivalent positive shift - int shift = ((y % bitLen) + bitLen) % bitLen; + int shift = positiveModulo(y, bitLen); if (shift == 0) { return bytes.clone(); @@ -3866,7 +3869,7 @@ public static byte[] leftShift(byte[] bytes, int y) { * @param y the shift amount in bits * @return shifted ByteString */ - public static ByteString leftShift(ByteString bytes, int y) { + public static ByteString leftShift(ByteString bytes, long y) { return new ByteString(leftShift(bytes.getBytes(), y)); } @@ -3874,8 +3877,8 @@ public static ByteString leftShift(ByteString bytes, int y) { * Performs PostgresSQL-style bitwise shift on UByte. * Overflow bits are masked to 8 bits. */ - public static UByte leftShift(UByte x, int y) { - int shift = ((y % 8) + 8) % 8; + public static UByte leftShift(UByte x, long y) { + int shift = positiveModulo(y, 8); int val = x.byteValue() & 0xFF; val = (y >= 0) ? (val << shift) & 0xFF : (val >> shift) & 0xFF; return UByte.valueOf((byte) val); @@ -3885,8 +3888,8 @@ public static UByte leftShift(UByte x, int y) { * Performs PostgresSQL-style bitwise shift on UShort. * Overflow bits are masked to 16 bits. */ - public static UShort leftShift(UShort x, int y) { - int shift = ((y % 16) + 16) % 16; + public static UShort leftShift(UShort x, long y) { + int shift = positiveModulo(y, 16); int val = x.shortValue() & 0xFFFF; val = (y >= 0) ? (val << shift) & 0xFFFF : (val >> shift) & 0xFFFF; return UShort.valueOf((short) val); @@ -3896,8 +3899,8 @@ public static UShort leftShift(UShort x, int y) { * Performs PostgresSQL-style bitwise shift on UInteger. * Overflow bits are masked to 32 bits. */ - public static UInteger leftShift(UInteger x, int y) { - int shift = ((y % 32) + 32) % 32; + public static UInteger leftShift(UInteger x, long y) { + int shift = positiveModulo(y, 32); long val = x.longValue() & 0xFFFFFFFFL; val = (y >= 0) ? (val << shift) & 0xFFFFFFFFL : (val >> shift) & 0xFFFFFFFFL; return UInteger.valueOf(val); @@ -3907,10 +3910,86 @@ public static UInteger leftShift(UInteger x, int y) { * Performs PostgresSQL-style bitwise shift on ULong. * Overflow bits are masked to 64 bits (long shifts naturally truncate). */ - public static ULong leftShift(ULong x, int y) { - int shift = ((y % 64) + 64) % 64; + public static ULong leftShift(ULong x, long y) { + int shift = positiveModulo(y, 64); + long val = x.longValue(); + // A negative shift amount shifts right; use a logical (unsigned) shift so + // the full-width ULong value is not sign-extended. + val = (y >= 0) ? val << shift : val >>> shift; + return ULong.valueOf(val); + } + + /** + * Performs PostgresSQL-style bitwise shift on a 32-bit integer. + * + * @param x the integer value to shift + * @param y the shift amount (positive: right shift, negative: left shift) + * @return the shifted integer + */ + public static int rightShift(int x, long y) { + int shift = positiveModulo(y, 32); // normalize to 0~31 + return y >= 0 ? x >> shift : x << shift; // arithmetic right shift + } + + /** + * Performs PostgresSQL-style bitwise shift on a 64-bit long value. + * + * @param x the long value to shift + * @param y the shift amount + * @return the shifted long value + */ + public static long rightShift(long x, long y) { + int shift = positiveModulo(y, 64); // normalize to 0~63 + return y >= 0 ? x >> shift : x << shift; + } + + // Right shift on binary (byte[]/ByteString) is intentionally not implemented: + // BINARY/VARBINARY operands are rejected for >> and RIGHTSHIFT until the + // endianness of bitwise shifts on binary is settled. See [CALCITE-7651]. + + /** + * Performs PostgresSQL-style bitwise shift on UByte. + * Overflow bits are masked to 8 bits. + */ + public static UByte rightShift(UByte x, long y) { + int shift = positiveModulo(y, 8); + int val = x.byteValue() & 0xFF; + val = (y >= 0) ? (val >> shift) & 0xFF : (val << shift) & 0xFF; + return UByte.valueOf((byte) val); + } + + /** + * Performs PostgresSQL-style bitwise shift on UShort. + * Overflow bits are masked to 16 bits. + */ + public static UShort rightShift(UShort x, long y) { + int shift = positiveModulo(y, 16); + int val = x.shortValue() & 0xFFFF; + val = (y >= 0) ? (val >> shift) & 0xFFFF : (val << shift) & 0xFFFF; + return UShort.valueOf((short) val); + } + + /** + * Performs PostgresSQL-style bitwise shift on UInteger. + * Overflow bits are masked to 32 bits. + */ + public static UInteger rightShift(UInteger x, long y) { + int shift = positiveModulo(y, 32); + long val = x.longValue() & 0xFFFFFFFFL; + val = (y >= 0) ? (val >> shift) & 0xFFFFFFFFL : (val << shift) & 0xFFFFFFFFL; + return UInteger.valueOf(val); + } + + /** + * Performs PostgresSQL-style bitwise shift on ULong. + * Overflow bits are masked to 64 bits (long shifts naturally truncate). + */ + public static ULong rightShift(ULong x, long y) { + int shift = positiveModulo(y, 64); long val = x.longValue(); - val = (y >= 0) ? val << shift : val >> shift; + // Use a logical (unsigned) right shift: ULong holds the full 64 bits, so + // the raw long may be negative and an arithmetic '>>' would sign-extend. + val = (y >= 0) ? val >>> shift : val << shift; return ULong.valueOf(val); } 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 b1f0c0d5044d..20a5e690ffe6 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 @@ -64,6 +64,7 @@ import org.apache.calcite.sql.type.OperandTypes; import org.apache.calcite.sql.type.ReturnTypes; import org.apache.calcite.sql.type.SqlOperandCountRanges; +import org.apache.calcite.sql.type.SqlOperandTypeChecker; import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.calcite.sql.type.SqlTypeName; @@ -1367,6 +1368,29 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { public static final SqlAggFunction BIT_XOR = new SqlBitOpAggFunction(SqlKind.BIT_XOR); + /** + * Operand type checker shared by the left shift operator ({@code <<}) and its + * function form ({@code LEFTSHIFT}). The first operand is the value being + * shifted (integer, binary or unsigned numeric) and the second is the integer + * shift amount. + */ + private static final SqlOperandTypeChecker SHIFT_OPERAND_TYPE_CHECKER = + OperandTypes.INTEGER_INTEGER + .or(OperandTypes.family(SqlTypeFamily.BINARY, SqlTypeFamily.INTEGER)) + .or(OperandTypes.family(SqlTypeFamily.UNSIGNED_NUMERIC, SqlTypeFamily.INTEGER)); + + /** + * Operand type checker for the right shift operator ({@code >>}) and its + * function form ({@code RIGHTSHIFT}). Like {@link #SHIFT_OPERAND_TYPE_CHECKER} + * but the value being shifted may only be integer or unsigned numeric, not + * binary. Binary right shift is intentionally excluded until the endianness of + * bitwise shifts on {@code BINARY}/{@code VARBINARY} is settled; see + * [CALCITE-7651]. + */ + private static final SqlOperandTypeChecker NON_BINARY_SHIFT_OPERAND_TYPE_CHECKER = + OperandTypes.INTEGER_INTEGER + .or(OperandTypes.family(SqlTypeFamily.UNSIGNED_NUMERIC, SqlTypeFamily.INTEGER)); + /** * {@code <<} (left shift) operator. */ @@ -1378,10 +1402,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { true, ReturnTypes.ARG0_NULLABLE, InferTypes.FIRST_KNOWN, - OperandTypes.or( - OperandTypes.family(SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER), - OperandTypes.family(SqlTypeFamily.BINARY, SqlTypeFamily.INTEGER), - OperandTypes.family(SqlTypeFamily.UNSIGNED_NUMERIC, SqlTypeFamily.INTEGER))); + SHIFT_OPERAND_TYPE_CHECKER); /** * left shift function. @@ -1391,10 +1412,30 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { "LEFTSHIFT", SqlKind.OTHER_FUNCTION, ReturnTypes.ARG0_NULLABLE, - OperandTypes.or( - OperandTypes.family(SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER), - OperandTypes.family(SqlTypeFamily.BINARY, SqlTypeFamily.INTEGER), - OperandTypes.family(SqlTypeFamily.UNSIGNED_NUMERIC, SqlTypeFamily.INTEGER))); + SHIFT_OPERAND_TYPE_CHECKER); + + /** + * {@code >>} (right shift) operator. + */ + public static final SqlBinaryOperator BIT_RIGHT_SHIFT = + new SqlBinaryOperator( + ">>", + SqlKind.OTHER, + 32, // Standard shift operator precedence + true, + ReturnTypes.ARG0_NULLABLE, + InferTypes.FIRST_KNOWN, + NON_BINARY_SHIFT_OPERAND_TYPE_CHECKER); + + /** + * right shift function. + */ + public static final SqlFunction RIGHTSHIFT = + SqlBasicFunction.create( + "RIGHTSHIFT", + SqlKind.OTHER_FUNCTION, + ReturnTypes.ARG0_NULLABLE, + NON_BINARY_SHIFT_OPERAND_TYPE_CHECKER); //------------------------------------------------------------- // WINDOW Aggregate Functions diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java index 97fb72cac001..ed74719ab332 100644 --- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java +++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserPos.java @@ -264,6 +264,20 @@ public boolean startsAt(SqlParserPos pos) { && columnNumber == pos.columnNumber; } + /** + * Returns whether this position ends exactly one column before another + * position begins, on the same line, with no characters (such as whitespace) + * in between. + * + * @param pos position that may immediately follow this one + * @return whether this position ends exactly one column before {@code pos} + * begins, on the same line + */ + public boolean endsImmediatelyBefore(SqlParserPos pos) { + return endLineNumber == pos.lineNumber + && endColumnNumber + 1 == pos.columnNumber; + } + /** Parser position for an identifier segment that is quoted. */ private static class QuotedParserPos extends SqlParserPos { QuotedParserPos(int startLineNumber, int startColumnNumber, diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 295d7d662e62..a994234ddeee 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -705,7 +705,8 @@ public enum BuiltInMethod { BIT_OR(SqlFunctions.class, "bitOr", long.class, long.class), BIT_XOR(SqlFunctions.class, "bitXor", long.class, long.class), BIT_NOT(SqlFunctions.class, "bitNot", long.class), - LEFT_SHIFT(SqlFunctions.class, "leftShift", int.class, int.class), + LEFT_SHIFT(SqlFunctions.class, "leftShift", int.class, long.class), + RIGHT_SHIFT(SqlFunctions.class, "rightShift", int.class, long.class), MODIFIABLE_TABLE_GET_MODIFIABLE_COLLECTION(ModifiableTable.class, "getModifiableCollection"), SCANNABLE_TABLE_SCAN(ScannableTable.class, "scan", DataContext.class), diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserPosTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserPosTest.java new file mode 100644 index 000000000000..ed6d07e77c6b --- /dev/null +++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserPosTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.sql.parser; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for {@link SqlParserPos}. + */ +public class SqlParserPosTest { + /** Tests {@link SqlParserPos#endsImmediatelyBefore(SqlParserPos)}. */ + @Test void testEndsImmediatelyBefore() { + // A single-character position ends immediately before the one in the next + // column, like the two '>' of a '>>' token. + final SqlParserPos col1 = new SqlParserPos(1, 1); + final SqlParserPos col2 = new SqlParserPos(1, 2); + assertThat(col1.endsImmediatelyBefore(col2), is(true)); + + // The relation is directional, not symmetric. + assertThat(col2.endsImmediatelyBefore(col1), is(false)); + + // A gap between the positions (e.g. whitespace, like '> >') does not + // qualify. + final SqlParserPos col3 = new SqlParserPos(1, 3); + assertThat(col1.endsImmediatelyBefore(col3), is(false)); + + // A position does not end immediately before itself. + assertThat(col1.endsImmediatelyBefore(col1), is(false)); + + // A multi-column position ends immediately before the position that starts + // one column after it ends. + final SqlParserPos cols1To2 = new SqlParserPos(1, 1, 1, 2); + assertThat(cols1To2.endsImmediatelyBefore(col3), is(true)); + assertThat(cols1To2.endsImmediatelyBefore(col2), is(false)); + + // Positions on different lines never qualify. + final SqlParserPos line2 = new SqlParserPos(2, 1); + assertThat(new SqlParserPos(1, 5).endsImmediatelyBefore(line2), is(false)); + } +} diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java index 959e2fabc258..9231437cf8ea 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java @@ -2045,19 +2045,30 @@ private long sqlTimestamp(String str) { return toLong(java.sql.Timestamp.valueOf(str)); } @Test void testLeftShift() { - // Test 1-byte array + // For every shift amount, cross-check the byte-array shift against the + // equivalent shift computed on the value's integer interpretation. The byte + // array is treated as a little-endian bit string of width 8 * length (index + // 0 is the least-significant byte), and leftShift always shifts left by the + // amount normalized modulo that width (so a negative amount wraps into range + // rather than reversing direction). byte[] data1 = {(byte) 0x0F}; // 00001111 for (int shift = -10; shift <= 10; shift++) { byte[] result = SqlFunctions.leftShift(data1.clone(), shift); - // Just verify it doesn't crash and returns correct length assertEquals(1, result.length); + int norm = Math.floorMod(shift, 8); + int expected = ((data1[0] & 0xFF) << norm) & 0xFF; + assertEquals((byte) expected, result[0]); } - // Test 2-byte array byte[] data2 = {(byte) 0x12, (byte) 0x34}; for (int shift = -18; shift <= 18; shift++) { byte[] result = SqlFunctions.leftShift(data2.clone(), shift); assertEquals(2, result.length); + int value = (data2[0] & 0xFF) | ((data2[1] & 0xFF) << 8); // little-endian + int norm = Math.floorMod(shift, 16); + int expected = (value << norm) & 0xFFFF; + assertEquals((byte) expected, result[0]); + assertEquals((byte) (expected >>> 8), result[1]); } // Verify specific known cases @@ -2068,6 +2079,14 @@ private long sqlTimestamp(String str) { SqlFunctions.leftShift(new byte[]{(byte) 0x40, (byte) 0x00}, 1)); } + @Test void testRightShift() { + // Scalar arithmetic (sign-preserving) right shift. Binary right shift is + // intentionally not supported (see [CALCITE-7651]), so there is no + // byte-array overload to exercise here. + assertEquals(2, SqlFunctions.rightShift(8, 2)); + assertEquals(-5, SqlFunctions.rightShift(-20, 2)); + } + @Test void testCombineQueryResults() { // Test combining two equal-length lists List list1 = Arrays.asList(1, 2, 3); 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 0fb4fa4055b6..24a6e875e182 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -11119,6 +11119,7 @@ private static int prec(SqlOperator op) { + "> SOME left\n" + ">= ALL left\n" + ">= SOME left\n" + + ">> left\n" + "BETWEEN ASYMMETRIC -\n" + "BETWEEN SYMMETRIC -\n" + "IN left\n" diff --git a/core/src/test/resources/sql/operator.iq b/core/src/test/resources/sql/operator.iq index 33731a08c4d2..41a470e5f934 100644 --- a/core/src/test/resources/sql/operator.iq +++ b/core/src/test/resources/sql/operator.iq @@ -121,6 +121,21 @@ WHERE comm IS NOT NULL LIMIT 4; !ok +# [CALCITE-7639] Add support for >> operator in Calcite +SELECT CAST(comm AS INTEGER) >> 2 AS foo FROM "scott".emp +WHERE comm IS NOT NULL LIMIT 4; ++-----+ +| FOO | ++-----+ +| 75 | +| 0 | +| 125 | +| 350 | ++-----+ +(4 rows) + +!ok + # [CALCITE-5531] COALESCE throws ClassCastException SELECT COALESCE(DATE '2021-07-08', DATE '2020-01-01') as d; +------------+ @@ -811,4 +826,20 @@ SELECT !ok +-- Bitwise RIGHT SHIFT operator `>>` +SELECT + 1 >> 0 AS shift1, + 2 >> 1 AS shift2, + 8 >> 2 AS shift3, + 32 >> 3 AS shift4, + CAST(16 AS SMALLINT) >> CAST(3 AS INTEGER) AS cast_shift; ++--------+--------+--------+--------+------------+ +| SHIFT1 | SHIFT2 | SHIFT3 | SHIFT4 | CAST_SHIFT | ++--------+--------+--------+--------+------------+ +| 1 | 1 | 2 | 4 | 2 | ++--------+--------+--------+--------+------------+ +(1 row) + +!ok + # End operator.iq diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 064d6fb95e40..e6d58eb84f62 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -2973,7 +2973,8 @@ In the following: | * | BITAND(value1, value2) | Returns the bitwise AND of *value1* and *value2*. *value1* and *value2* must both be integer or binary values. Binary values must be of the same length. | * | BITOR(value1, value2) | Returns the bitwise OR of *value1* and *value2*. *value1* and *value2* must both be integer or binary values. Binary values must be of the same length. | * | BITXOR(value1, value2) | Returns the bitwise XOR of *value1* and *value2*. *value1* and *value2* must both be integer or binary values. Binary values must be of the same length. -| * | LEFTSHIFT(value1, value2) | Returns the result of left-shifting *value1* by *value2* bits. *value1* can be integer, unsigned integer, or binary. For binary, the result has the same length as *value1*. The shift amount *value2* is normalized using modulo arithmetic based on the bit width of *value1*. For integers, this uses modulo 32; for binary types, it uses modulo (8 × byte_length). Negative shift amounts are converted to equivalent positive shifts through this modulo operation. For example, `LEFTSHIFT(1, -2)` returns `1073741824` (equivalent to `1 << 30`), and `LEFTSHIFT(8, -1)` returns `0` due to overflow. +| * | LEFTSHIFT(value1, value2) | Returns the result of left-shifting *value1* by *value2* bits. *value1* can be integer, unsigned integer, or binary. For binary, the result has the same length as *value1*. The shift amount *value2* is normalized using modulo arithmetic: for signed integer types the modulus is 32 for `TINYINT`, `SMALLINT` and `INTEGER` (all backed by a 32-bit representation) and 64 for `BIGINT`; for unsigned integer types it matches the type's bit width (modulo 8, 16, 32 or 64); for binary types it is modulo (8 × N), where N is the actual length in bytes of the *value1* value — for a variable-length `VARBINARY` value this is the length of the value itself, not its declared maximum. For integer and unsigned types the sign of *value2* selects the direction: a non-negative amount shifts left and a negative amount shifts right by the normalized magnitude (for example, `LEFTSHIFT(1, -2)` returns `0`, a right shift by 30, and `LEFTSHIFT(8, -1)` returns `0`). For binary the shift is always to the left; a negative *value2* is simply folded into the range [0, 8 × N) by the same modulo. +| * | RIGHTSHIFT(value1, value2) | Returns the result of right-shifting *value1* by *value2* bits. For signed integers the shift is arithmetic (the sign bit is preserved). *value1* can be integer or unsigned integer (binary right shift is not yet supported). The shift amount *value2* is normalized using modulo arithmetic: for signed integer types the modulus is 32 for `TINYINT`, `SMALLINT` and `INTEGER` (all backed by a 32-bit representation) and 64 for `BIGINT`; for unsigned integer types it matches the type's bit width (modulo 8, 16, 32 or 64). The sign of *value2* selects the direction: a non-negative amount shifts right and a negative amount shifts left by the normalized magnitude (for example, `RIGHTSHIFT(1024, 2)` returns `256`, `RIGHTSHIFT(-20, 2)` returns `-5`, and `RIGHTSHIFT(1, -2)` returns `1073741824`, a left shift by 30). | * | BITNOT(value) | Returns the bitwise NOT of *value*. *value* must be either an integer type or a binary value. | f | BITAND_AGG(value) | Equivalent to `BIT_AND(value)` | f | BITOR_AGG(value) | Equivalent to `BIT_OR(value)` diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java index bb7566c9904c..039f094b005b 100644 --- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java +++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java @@ -1254,6 +1254,25 @@ private void checkLarge(int n) { .ok("((NOT (NOT (`A` = `B`))) OR (NOT (NOT (`C` = `D`))))"); } + @Test void testShiftOperators() { + expr("1 << 2") + .ok("(1 << 2)"); + // '>>' is recognized as two adjacent '>' tokens. + expr("1 >> 2") + .ok("(1 >> 2)"); + + // '<<' and '>>' have the same precedence and are left-associative. + expr("a << b >> c") + .ok("((`A` << `B`) >> `C`)"); + expr("a >> b >> c") + .ok("((`A` >> `B`) >> `C`)"); + + // The two '>' of a right shift must be adjacent, so "a > > b" (with a space + // between the '>' characters) is not parsed as a right shift. + expr("a ^>^ > b") + .fails("(?s).*Encountered \"> >\" at line 1, column 3\\..*"); + } + @Test void testIsBooleans() { String[] inOuts = {"NULL", "TRUE", "FALSE", "UNKNOWN"}; 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 2119f79df20e..243bfca489e5 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -16958,6 +16958,7 @@ private static void checkLogicalOrFunc(SqlOperatorFixture f) { f.checkType("CAST(2 AS SMALLINT) << CAST(3 AS SMALLINT)", "SMALLINT NOT NULL"); f.checkType("CAST(2 AS INTEGER) << CAST(3 AS INTEGER)", "INTEGER NOT NULL"); f.checkType("CAST(2 AS BIGINT) << CAST(3 AS BIGINT)", "BIGINT NOT NULL"); + f.checkScalar("CAST(2 AS BIGINT) << CAST(3 AS BIGINT)", "16", "BIGINT NOT NULL"); // === BigInt shifts with explicit BIGINT inputs === f.checkScalar("CAST(1 AS BIGINT) << 62", BigInteger.ONE.shiftLeft(62).toString(), @@ -17005,8 +17006,16 @@ private static void checkLogicalOrFunc(SqlOperatorFixture f) { "INTEGER UNSIGNED NOT NULL"); f.checkScalar("CAST(1 AS INTEGER UNSIGNED) << 31", "2147483648", "INTEGER UNSIGNED NOT NULL"); f.checkScalar("CAST(1 AS INTEGER UNSIGNED) << -1", "0", "INTEGER UNSIGNED NOT NULL"); + // BIGINT UNSIGNED with the high bit set (2^63, built via 1 << 63), shifted + // left by a negative amount (i.e. right by 60): the implied right shift must + // be logical, not arithmetic (the raw long is negative). + f.checkScalar("CAST(1 AS BIGINT UNSIGNED) << 63 << -4", + "8", "BIGINT UNSIGNED NOT NULL"); // === Negative shift counts === + // A negative left shift shifts right by the normalized magnitude (here + // 32 - 2 = 30); it is not the same as a right shift by the given amount. + f.checkScalar("1 << -2", "0", "INTEGER NOT NULL"); // 1 >> 30 f.checkScalar("8 << -1", "0", "INTEGER NOT NULL"); f.checkScalar("16 << -2", "0", "INTEGER NOT NULL"); @@ -17078,6 +17087,7 @@ private static void checkLogicalOrFunc(SqlOperatorFixture f) { f.checkType("LEFTSHIFT(CAST(2 AS SMALLINT), CAST(3 AS SMALLINT))", "SMALLINT NOT NULL"); f.checkType("LEFTSHIFT(CAST(2 AS INTEGER), CAST(3 AS INTEGER))", "INTEGER NOT NULL"); f.checkType("LEFTSHIFT(CAST(2 AS BIGINT), CAST(3 AS BIGINT))", "BIGINT NOT NULL"); + f.checkScalar("LEFTSHIFT(CAST(2 AS BIGINT), CAST(3 AS BIGINT))", "16", "BIGINT NOT NULL"); // === BigInt shifts with explicit BIGINT inputs === f.checkScalar("LEFTSHIFT(CAST(1 AS BIGINT), 62)", @@ -17158,6 +17168,193 @@ private static void checkLogicalOrFunc(SqlOperatorFixture f) { f.checkNull("LEFTSHIFT(CAST(NULL AS INTEGER UNSIGNED), 2)"); } + /** + * Test cases for + * [CALCITE-7639] + * Support bitwise right shift (>>) operator and RIGHTSHIFT function. + */ + @Test void testRightShiftScalarFunc() { + final SqlOperatorFixture f = fixture(); + f.setFor(SqlStdOperatorTable.BIT_RIGHT_SHIFT, VmName.EXPAND); + + // === Basic functionality === + f.checkScalar("8 >> 2", "2", "INTEGER NOT NULL"); + f.checkScalar("1024 >> 10", "1", "INTEGER NOT NULL"); + f.checkScalar("0 >> 5", "0", "INTEGER NOT NULL"); + + // === Type coercion and signed (arithmetic) behavior === + f.checkScalar("CAST(16 AS INTEGER) >> CAST(3 AS BIGINT)", "2", "INTEGER NOT NULL"); + f.checkScalar("-20 >> 2", "-5", "INTEGER NOT NULL"); + f.checkScalar("-40 >> 3", "-5", "INTEGER NOT NULL"); + f.checkScalar("CAST(-20 AS TINYINT) >> CAST(2 AS TINYINT)", "-5", "TINYINT NOT NULL"); + + // === Verify return type matches first argument type === + f.checkType("CAST(8 AS TINYINT) >> CAST(2 AS TINYINT)", "TINYINT NOT NULL"); + f.checkType("CAST(8 AS SMALLINT) >> CAST(2 AS SMALLINT)", "SMALLINT NOT NULL"); + f.checkType("CAST(8 AS INTEGER) >> CAST(2 AS INTEGER)", "INTEGER NOT NULL"); + f.checkType("CAST(8 AS BIGINT) >> CAST(2 AS BIGINT)", "BIGINT NOT NULL"); + f.checkScalar("CAST(8 AS BIGINT) >> CAST(2 AS BIGINT)", "2", "BIGINT NOT NULL"); + + // === BigInt shifts with explicit BIGINT inputs (arithmetic/sign-preserving) === + f.checkScalar("CAST(4611686018427387904 AS BIGINT) >> 62", "1", "BIGINT NOT NULL"); // 2^62 + f.checkScalar("CAST(9223372036854775807 AS BIGINT) >> 1", + BigInteger.valueOf(Long.MAX_VALUE).shiftRight(1).toString(), "BIGINT NOT NULL"); + f.checkScalar("CAST(-1 AS BIGINT) >> 63", "-1", "BIGINT NOT NULL"); // sign bit preserved + f.checkScalar("CAST(-1 AS BIGINT) >> 1", "-1", "BIGINT NOT NULL"); + f.checkScalar("CAST(1000000000 AS BIGINT) >> 5", "31250000", "BIGINT NOT NULL"); + + // === Shift amount normalized using modulo of the bit width === + f.checkScalar("CAST(1024 AS BIGINT) >> 64", "1024", "BIGINT NOT NULL"); // 64 % 64 = 0 + f.checkScalar("CAST(1024 AS BIGINT) >> 74", "1", "BIGINT NOT NULL"); // 74 % 64 = 10 + f.checkScalar("1 >> 32", "1", "INTEGER NOT NULL"); // 32 % 32 = 0 + f.checkScalar("123 >> 60", "0", "INTEGER NOT NULL"); // 60 % 32 = 28 + + // === Unsigned types === + f.checkScalar("CAST(252 AS TINYINT UNSIGNED) >> 2", "63", "TINYINT UNSIGNED NOT NULL"); + f.checkScalar("CAST(65280 AS SMALLINT UNSIGNED) >> 8", "255", "SMALLINT UNSIGNED NOT NULL"); + f.checkScalar("CAST(4294901760 AS INTEGER UNSIGNED) >> 16", "65535", + "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(2147483648 AS INTEGER UNSIGNED) >> 31", "1", "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("CAST(1 AS INTEGER UNSIGNED) >> -1", "2147483648", "INTEGER UNSIGNED NOT NULL"); + // BIGINT UNSIGNED with the high bit set (2^63, built via 1 << 63): the + // right shift must be logical, not arithmetic (the raw long is negative). + f.checkScalar("CAST(1 AS BIGINT UNSIGNED) << 63 >> 4", + "576460752303423488", "BIGINT UNSIGNED NOT NULL"); + + // A BIGINT shift amount is accepted (INTEGER family), but an unsigned shift + // amount is not: the second operand must be a signed integer type. + f.checkScalar("CAST(8 AS INTEGER) >> CAST(2 AS BIGINT)", "2", "INTEGER NOT NULL"); + f.checkFails("^8 >> CAST(2 AS INTEGER UNSIGNED)^", + "Cannot apply '>>' to arguments of type ' >> '\\. " + + "Supported form\\(s\\): ' >> '\\n" + + "' >> '", + false); + + // === Negative shift counts (normalized via modulo, then shifted the other way) === + // A negative right shift shifts left by the normalized magnitude (here + // 32 - 2 = 30); it is not the same as a left shift by the given amount. + f.checkScalar("1 >> -2", "1073741824", "INTEGER NOT NULL"); // 1 << 30 + f.checkScalar("8 >> -1", "0", "INTEGER NOT NULL"); + f.checkScalar("16 >> -2", "0", "INTEGER NOT NULL"); + + // === Shift by zero and large shifts === + f.checkScalar("0 >> 32", "0", "INTEGER NOT NULL"); + f.checkScalar("0 >> 100", "0", "INTEGER NOT NULL"); + + // === Binary operands are not supported === + // Unlike '<<', binary right shift is intentionally rejected until the + // endianness of bitwise shifts on binary is settled (see [CALCITE-7651]). + // A binary literal such as X'FF' already has type BINARY(1). + f.checkFails("^X'FF' >> 1^", + "Cannot apply '>>' to arguments of type ' >> '\\. " + + "Supported form\\(s\\): ' >> '\\n" + + "' >> '", + false); + f.checkFails("^CAST(X'FF' AS VARBINARY) >> 1^", + "Cannot apply '>>' to arguments of type ' >> '\\. " + + "Supported form\\(s\\): ' >> '\\n" + + "' >> '", + false); + + // === Invalid argument types === + f.checkFails("^1.2 >> 2^", + "Cannot apply '>>' to arguments of type ' >> '\\. Supported " + + "form\\(s\\): ' >> '\\n' " + + ">> '", + false); + + // === Null propagation === + f.checkNull("CAST(NULL AS INTEGER) >> 5"); + f.checkNull("10 >> CAST(NULL AS INTEGER)"); + f.checkNull("CAST(NULL AS INTEGER) >> CAST(NULL AS INTEGER)"); + f.checkNull("CAST(NULL AS INTEGER UNSIGNED) >> 2"); + } + + @Test void testRightShiftFunctionCall() { + final SqlOperatorFixture f = fixture(); + f.setFor(SqlStdOperatorTable.BIT_RIGHT_SHIFT, VmName.EXPAND); + + // === Basic functionality === + f.checkScalar("RIGHTSHIFT(8, 2)", "2", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(1024, 10)", "1", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(0, 5)", "0", "INTEGER NOT NULL"); + + // === Type coercion and signed (arithmetic) behavior === + f.checkScalar("RIGHTSHIFT(CAST(16 AS INTEGER), CAST(3 AS BIGINT))", "2", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(-20, 2)", "-5", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(-40, 3)", "-5", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(-20 AS TINYINT), CAST(2 AS TINYINT))", "-5", "TINYINT NOT NULL"); + + // === Verify return type matches first argument type === + f.checkType("RIGHTSHIFT(CAST(8 AS TINYINT), CAST(2 AS TINYINT))", "TINYINT NOT NULL"); + f.checkType("RIGHTSHIFT(CAST(8 AS SMALLINT), CAST(2 AS SMALLINT))", "SMALLINT NOT NULL"); + f.checkType("RIGHTSHIFT(CAST(8 AS INTEGER), CAST(2 AS INTEGER))", "INTEGER NOT NULL"); + f.checkType("RIGHTSHIFT(CAST(8 AS BIGINT), CAST(2 AS BIGINT))", "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(8 AS BIGINT), CAST(2 AS BIGINT))", "2", "BIGINT NOT NULL"); + + // === BigInt shifts with explicit BIGINT inputs === + f.checkScalar("RIGHTSHIFT(CAST(4611686018427387904 AS BIGINT), 62)", "1", "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(9223372036854775807 AS BIGINT), 1)", + BigInteger.valueOf(Long.MAX_VALUE).shiftRight(1).toString(), "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(-1 AS BIGINT), 63)", "-1", "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(-1 AS BIGINT), 1)", "-1", "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(1000000000 AS BIGINT), 5)", "31250000", "BIGINT NOT NULL"); + + // === Shift amount normalized using modulo of the bit width === + f.checkScalar("RIGHTSHIFT(CAST(1024 AS BIGINT), 64)", "1024", "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(1024 AS BIGINT), 74)", "1", "BIGINT NOT NULL"); + f.checkScalar("RIGHTSHIFT(1, 32)", "1", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(123, 60)", "0", "INTEGER NOT NULL"); + + // === Unsigned types === + f.checkScalar("RIGHTSHIFT(CAST(252 AS TINYINT UNSIGNED), 2)", "63", + "TINYINT UNSIGNED NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(65280 AS SMALLINT UNSIGNED), 8)", "255", + "SMALLINT UNSIGNED NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(4294901760 AS INTEGER UNSIGNED), 16)", "65535", + "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(2147483648 AS INTEGER UNSIGNED), 31)", "1", + "INTEGER UNSIGNED NOT NULL"); + f.checkScalar("RIGHTSHIFT(CAST(1 AS INTEGER UNSIGNED), -1)", "2147483648", + "INTEGER UNSIGNED NOT NULL"); + + // === Negative shifts === + f.checkScalar("RIGHTSHIFT(8, -1)", "0", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(16, -2)", "0", "INTEGER NOT NULL"); + + // === Large shifts === + f.checkScalar("RIGHTSHIFT(0, 32)", "0", "INTEGER NOT NULL"); + f.checkScalar("RIGHTSHIFT(0, 100)", "0", "INTEGER NOT NULL"); + + // === Binary operands are not supported === + // Unlike LEFTSHIFT, binary right shift is intentionally rejected until the + // endianness of bitwise shifts on binary is settled (see [CALCITE-7651]). + // A binary literal such as X'FF' already has type BINARY(1). + f.checkFails("^RIGHTSHIFT(X'FF', 1)^", + "Cannot apply 'RIGHTSHIFT' to arguments of type 'RIGHTSHIFT\\(, \\)'\\. Supported form\\(s\\): 'RIGHTSHIFT\\(, \\)'\\n'RIGHTSHIFT\\(, \\)'", + false); + f.checkFails("^RIGHTSHIFT(CAST(X'FF' AS VARBINARY), 1)^", + "Cannot apply 'RIGHTSHIFT' to arguments of type 'RIGHTSHIFT\\(, \\)'\\. Supported form\\(s\\): 'RIGHTSHIFT\\(, \\)'\\n'RIGHTSHIFT\\(, \\)'", + false); + + // === Invalid types === + f.checkFails("^RIGHTSHIFT(1.2, 2)^", + "Cannot apply 'RIGHTSHIFT' to arguments of type 'RIGHTSHIFT\\(, \\)'\\. Supported form\\(s\\): 'RIGHTSHIFT\\(, \\)'\\n'RIGHTSHIFT\\(, \\)'", + false); + // A BIGINT shift amount is accepted, but an unsigned shift amount is not: + // the second argument must be a signed integer type. + f.checkScalar("RIGHTSHIFT(8, CAST(2 AS BIGINT))", "2", "INTEGER NOT NULL"); + f.checkFails("^RIGHTSHIFT(8, CAST(2 AS INTEGER UNSIGNED))^", + "Cannot apply 'RIGHTSHIFT' to arguments of type 'RIGHTSHIFT\\(, \\)'\\. Supported form\\(s\\): 'RIGHTSHIFT\\(, \\)'\\n'RIGHTSHIFT\\(, \\)'", + false); + + // === Nulls === + f.checkNull("RIGHTSHIFT(CAST(NULL AS INTEGER), 5)"); + f.checkNull("RIGHTSHIFT(10, CAST(NULL AS INTEGER))"); + f.checkNull("RIGHTSHIFT(CAST(NULL AS INTEGER), CAST(NULL AS INTEGER))"); + f.checkNull("RIGHTSHIFT(CAST(NULL AS INTEGER UNSIGNED), 2)"); + } + /** * Test cases for * [CALCITE-7184]