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]