This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new cb24d30828 [CALCITE-7639] Support bitwise right shift (>>) operator 
and RIGHTSHIFT function
cb24d30828 is described below

commit cb24d308280783c06750b6e8f3e971d4fca2fd80
Author: Niels Pardon <[email protected]>
AuthorDate: Fri Jul 24 07:59:07 2026 +0200

    [CALCITE-7639] Support bitwise right shift (>>) operator and RIGHTSHIFT 
function
    
    Mirrors the left-shift work in CALCITE-7109 to close a gap in the umbrella
    issue CALCITE-5087. Adds:
    
      * `>>` (SqlStdOperatorTable.BIT_RIGHT_SHIFT), a signed/arithmetic right
        shift (Java `>>`), symmetric to `<<` (precedence 32, left-assoc,
        ReturnTypes.ARG0_NULLABLE, InferTypes.FIRST_KNOWN).
      * `RIGHTSHIFT(x, n)` scalar function, mirroring `LEFTSHIFT`.
      * SqlFunctions.rightShift(...) runtime overloads (int, long, and the joou
        unsigned types), plus BuiltInMethod.RIGHT_SHIFT and the RexImpTable
        registrations for both operator and function.
    
    Operands are limited to integer and unsigned numeric types. Unlike `<<`,
    binary (BINARY/VARBINARY) right shift is intentionally rejected at
    validation until the endianness of bitwise shifts on binary is settled;
    that follow-up is tracked in CALCITE-7651.
    
    A greedy `>>` token cannot be added: the lexer would also match the two `>`
    that close nested angle-bracket types (e.g. MAP<INT, MAP<INT, INT>>),
    breaking type parsing. `>>` is therefore recognized in expression context as
    two adjacent `>` tokens via LOOKAHEAD(2) in BinaryRowOperator, leaving type
    parsing unchanged. Because there is no dedicated token, the SQL advisor
    advertises `>` rather than `>>`, so SqlAdvisorTest is not modified.
    
    Scope is limited to `>>` (arithmetic). The logical/fill-zero `>>>`
    (RIGHT_SHIFT_FILL_ZERO) remains a possible follow-up.
    
    Tests: SqlOperatorTest (operator + function forms), SqlFunctionsTest,
    operator.iq, and the operator-precedence dump in SqlValidatorTest; docs in
    site/_docs/reference.md.
---
 core/src/main/codegen/templates/Parser.jj          |  29 +++
 .../calcite/adapter/enumerable/RexImpTable.java    |  14 ++
 .../org/apache/calcite/runtime/SqlFunctions.java   | 135 +++++++++++---
 .../calcite/sql/fun/SqlStdOperatorTable.java       |  57 +++++-
 .../apache/calcite/sql/parser/SqlParserPos.java    |  14 ++
 .../org/apache/calcite/util/BuiltInMethod.java     |   3 +-
 .../calcite/sql/parser/SqlParserPosTest.java       |  57 ++++++
 .../org/apache/calcite/test/SqlFunctionsTest.java  |  25 ++-
 .../org/apache/calcite/test/SqlValidatorTest.java  |   1 +
 core/src/test/resources/sql/operator.iq            |  31 ++++
 site/_docs/reference.md                            |   3 +-
 .../apache/calcite/sql/parser/SqlParserTest.java   |  19 ++
 .../org/apache/calcite/test/SqlOperatorTest.java   | 197 +++++++++++++++++++++
 13 files changed, 544 insertions(+), 41 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index d3c1159741..ce69124c4b 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.
+     *
+     * <p>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".
     *
@@ -8487,6 +8505,17 @@ SqlBinaryOperator BinaryRowOperator() :
     // <IN> is handled as a special case
     <EQ> { return SqlStdOperatorTable.EQUALS; }
 |   <LEFTSHIFT> { 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<INT, MAP<INT, INT>>), 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))) })
+    <GT> <GT> { return SqlStdOperatorTable.BIT_RIGHT_SHIFT; }
 |   <GT> { return SqlStdOperatorTable.GREATER_THAN; }
 |   <LT> { return SqlStdOperatorTable.LESS_THAN; }
 |   <LE> { 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 bf5de12e74..2ad2be2893 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;
@@ -514,6 +515,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;
@@ -922,6 +924,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 c41ab84d71..fd4101b5a4 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -3789,6 +3789,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.
    *
@@ -3796,8 +3811,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
   }
 
@@ -3810,23 +3825,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.
@@ -3836,7 +3839,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];
     }
@@ -3845,7 +3848,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();
@@ -3883,7 +3886,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));
   }
 
@@ -3891,8 +3894,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);
@@ -3902,8 +3905,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);
@@ -3913,8 +3916,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);
@@ -3924,10 +3927,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 b1f0c0d504..20a5e690ff 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
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7651";>[CALCITE-7651]</a>.
+   */
+  private static final SqlOperandTypeChecker 
NON_BINARY_SHIFT_OPERAND_TYPE_CHECKER =
+      OperandTypes.INTEGER_INTEGER
+          .or(OperandTypes.family(SqlTypeFamily.UNSIGNED_NUMERIC, 
SqlTypeFamily.INTEGER));
+
   /**
    * <code>{@code <<}</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>{@code >>}</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 97fb72cac0..ed74719ab3 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 3c3a3e363e..629357b4e0 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -714,7 +714,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 0000000000..ed6d07e77c
--- /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 959e2fabc2..9231437cf8 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<Integer> 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 08667c036b..7bfc58eafe 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -11467,6 +11467,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 33731a08c4..41a470e5f9 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 b1f67eeef1..56fbf8a86c 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -3001,7 +3001,8 @@ ### Dialect-specific Operators
 | * | 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.  [...]
+| * | 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 [...]
+| * | 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 typ [...]
 | * | 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 9916e0f25e..394aecf26f 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 d710b516aa..8ddefaa32b 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -16955,6 +16955,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(),
@@ -17002,8 +17003,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");
 
@@ -17075,6 +17084,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)",
@@ -17155,6 +17165,193 @@ private static void 
checkLogicalOrFunc(SqlOperatorFixture f) {
     f.checkNull("LEFTSHIFT(CAST(NULL AS INTEGER UNSIGNED), 2)");
   }
 
+  /**
+   * Test cases for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7639";>[CALCITE-7639]
+   * Support bitwise right shift (&gt;&gt;) operator and RIGHTSHIFT 
function</a>.
+   */
+  @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 '<INTEGER> >> <INTEGER 
UNSIGNED>'\\. "
+            + "Supported form\\(s\\): '<INTEGER> >> <INTEGER>'\\n"
+            + "'<UNSIGNED_NUMERIC> >> <INTEGER>'",
+        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 '<BINARY\\(1\\)> >> 
<INTEGER>'\\. "
+            + "Supported form\\(s\\): '<INTEGER> >> <INTEGER>'\\n"
+            + "'<UNSIGNED_NUMERIC> >> <INTEGER>'",
+        false);
+    f.checkFails("^CAST(X'FF' AS VARBINARY) >> 1^",
+        "Cannot apply '>>' to arguments of type '<VARBINARY> >> <INTEGER>'\\. "
+            + "Supported form\\(s\\): '<INTEGER> >> <INTEGER>'\\n"
+            + "'<UNSIGNED_NUMERIC> >> <INTEGER>'",
+        false);
+
+    // === Invalid argument types ===
+    f.checkFails("^1.2 >> 2^",
+        "Cannot apply '>>' to arguments of type '<DECIMAL\\(2, 1\\)> >> 
<INTEGER>'\\. Supported "
+            + "form\\(s\\): '<INTEGER> >> <INTEGER>'\\n'<UNSIGNED_NUMERIC> "
+            + ">> <INTEGER>'",
+        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\\(<BINARY\\(1\\)>, <INTEGER>\\)'\\. Supported form\\(s\\): 
'RIGHTSHIFT\\(<INTEGER>, <INTEGER>\\)'\\n'RIGHTSHIFT\\(<UNSIGNED_NUMERIC>, 
<INTEGER>\\)'",
+        false);
+    f.checkFails("^RIGHTSHIFT(CAST(X'FF' AS VARBINARY), 1)^",
+        "Cannot apply 'RIGHTSHIFT' to arguments of type 
'RIGHTSHIFT\\(<VARBINARY>, <INTEGER>\\)'\\. Supported form\\(s\\): 
'RIGHTSHIFT\\(<INTEGER>, <INTEGER>\\)'\\n'RIGHTSHIFT\\(<UNSIGNED_NUMERIC>, 
<INTEGER>\\)'",
+        false);
+
+    // === Invalid types ===
+    f.checkFails("^RIGHTSHIFT(1.2, 2)^",
+        "Cannot apply 'RIGHTSHIFT' to arguments of type 
'RIGHTSHIFT\\(<DECIMAL\\(2, 1\\)>, <INTEGER>\\)'\\. Supported form\\(s\\): 
'RIGHTSHIFT\\(<INTEGER>, <INTEGER>\\)'\\n'RIGHTSHIFT\\(<UNSIGNED_NUMERIC>, 
<INTEGER>\\)'",
+        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\\(<INTEGER>, <INTEGER UNSIGNED>\\)'\\. Supported form\\(s\\): 
'RIGHTSHIFT\\(<INTEGER>, <INTEGER>\\)'\\n'RIGHTSHIFT\\(<UNSIGNED_NUMERIC>, 
<INTEGER>\\)'",
+        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
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7184";>[CALCITE-7184]


Reply via email to