[CALCITE-7639] Support bitwise right shift (>>) operator and RIGHTSHIFT function#5073
[CALCITE-7639] Support bitwise right shift (>>) operator and RIGHTSHIFT function#5073nielspardon wants to merge 11 commits into
Conversation
bb53cb6 to
a6d5390
Compare
…FT 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, and the same
INTEGER / BINARY / UNSIGNED_NUMERIC operand families).
* `RIGHTSHIFT(x, n)` scalar function, mirroring `LEFTSHIFT`.
* SqlFunctions.rightShift(...) runtime overloads (int, long, byte[],
ByteString, and the joou unsigned types), plus BuiltInMethod.RIGHT_SHIFT
and the RexImpTable registrations for both operator and function.
Unlike `<<`, 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.
a6d5390 to
d765854
Compare
… out shift operand checker
Address review feedback on the right shift PR:
* Parser.jj: replace the LOOKAHEAD(2) on the two '>' tokens with a
semantic lookahead that checks the tokens are immediately adjacent
(same line, adjacent columns). Previously '>>' matched even when the
two '>' were separated by whitespace, so 'a > > b' was parsed as a
right shift; it is now a syntax error, falling through to the single
'>' (greater-than) alternative.
* SqlStdOperatorTable: factor the duplicated operand type checker shared
by '<<', '>>', LEFTSHIFT and RIGHTSHIFT into a single
SHIFT_OPERAND_TYPE_CHECKER constant.
* SqlParserTest: add testShiftOperators covering '<<'/'>>' parsing,
precedence and left-associativity, and rejection of 'a > > b'.
| // 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); |
There was a problem hiding this comment.
Since LEFTSHIFT supports negative amounts, isn't RIGHTSHIFT(a, b) the same as LEFTSHIFT(a, -b)?
There was a problem hiding this comment.
Good question, but no — they aren't equivalent under the current (released) semantics. Counterexample, both values from the existing tests: RIGHTSHIFT(8, 1) = 4, whereas LEFTSHIFT(8, -1) = 0.
The reason is how the runtime handles the shift amount: it normalizes the magnitude with ((amount % w) + w) % w (where w is the type's bit width, e.g. 32 for INTEGER) and uses only the amount's sign to pick the direction. So for LEFTSHIFT(a, -b) the amount is -b, and its normalized magnitude is w - (b mod w) — not b. In other words LEFTSHIFT(a, -b) shifts by w - b, not by b. Concretely for INTEGER, -1 becomes a shift of 31: LEFTSHIFT(8, -1) = 8 >> 31 = 0, while RIGHTSHIFT(8, 1) = 8 >> 1 = 4. (BINARY differs again: the byte[] path ignores the sign entirely and always shifts in its native direction.)
That negative-amount behavior of LEFTSHIFT shipped in 1.41.0/1.42.0 (CALCITE-7109), so making the equivalence hold would mean changing released semantics — a separate change, out of scope here.
That said, your instinct points at real duplication: leftShift/rightShift differ only in which way the ternary points. Happy to factor the int/long/unsigned bodies through a shared private helper in a follow-up (or here) to cut that down, without touching behavior.
…oken check
Address review feedback: factor the '>>' token-adjacency check used by the
right shift operator into a reusable SqlParserPos.adjacent(SqlParserPos)
method, rather than open-coding the line/column comparison in the parser.
* SqlParserPos: add adjacent(SqlParserPos), which reports whether two
positions are immediately adjacent (one ends exactly where the other
begins, with no characters in between).
* Parser.jj: add a plain pos(Token) helper and use
pos(getToken(1)).adjacent(pos(getToken(2))) in the BIT_RIGHT_SHIFT
lookahead. pos is a plain method (not JAVACODE) so it can be called
from generated lookahead code, which does not declare
throws ParseException.
* SqlParserPosTest: add a unit test for SqlParserPos.adjacent.
| * @param y the long shift amount | ||
| * @return the shifted value as long | ||
| */ | ||
| public static long rightShift(int x, long y) { |
There was a problem hiding this comment.
you could only implement the long shift amount by casting any other type to long in the generated enumerable code.
Is there a test with a >> CAST(x AS UNSIGNED)? If not, there should be.
There was a problem hiding this comment.
Done. All leftShift/rightShift overloads now take a long shift amount, so an int amount widens automatically during codegen (EnumUtils.call resolves by widening) and a single amount type covers every value type — which let me drop the redundant (int, long) overloads and answers "why no long-y for these?" too.
It also closes a real gap: there was no (long, long) overload, so an executed BIGINT >> BIGINT would have failed to resolve at code generation — only checkType (which doesn't execute) was covering it. I added executed BIGINT-by-BIGINT cases for both the operator and function forms.
On the unsigned shift amount: I added a test, and it's currently rejected at validation — the second operand's family is signed INTEGER, so a >> CAST(x AS INTEGER UNSIGNED) is a Cannot apply error today (documented via checkFails). I left that as-is, since allowing it would be a separate change that also touches the released LEFTSHIFT; happy to file a follow-up if you'd like unsigned amounts supported.
There was a problem hiding this comment.
Please file an issue about unsigned amounts not being supported. I think you will agree that's not normal.
But if this implementation will need to be altered to support that better, maybe you can prepare it now.
I suspect the right way will be to cast any of these unsigned values to a long. The overflow for BIGINT UNSIGNED may require special handling.
There was a problem hiding this comment.
Filed as CALCITE-7648: https://issues.apache.org/jira/browse/CALCITE-7648
I've kept this PR scoped to the current behaviour (an unsigned amount is rejected, which the existing checkFails tests pin down) and captured your suggested approach in the ticket: widen the operand type checker to accept UNSIGNED_NUMERIC for the second operand, cast the amount to long in the generated enumerable code, with special handling for a BIGINT UNSIGNED amount whose high bit is set (it exceeds Long.MAX_VALUE). Happy to pick that up as the follow-up.
…oorMod, unsigned-amount tests
Address further review feedback:
* SqlParserPos: replace the symmetric adjacent(SqlParserPos) with the
directional endsImmediatelyBefore(SqlParserPos), which is more precise
for the right-shift token check and is now used directly by the parser.
* SqlFunctions: replace the hand-rolled ((y % w) + w) % w shift-amount
normalization with Math.floorMod(y, w) throughout leftShift/rightShift.
* SqlOperatorTest: add tests showing a BIGINT shift amount is accepted
while an unsigned shift amount is rejected at validation, for both the
>> operator and the RIGHTSHIFT function.
…functions
Address review feedback on the inconsistent shift-amount overloads: the
int-valued shift methods had both an int and a long shift-amount variant,
while the long and unsigned variants only had an int shift amount, and no
(long, long) overload existed at all (so an executed BIGINT >> BIGINT would
fail to resolve at code generation).
Make every leftShift/rightShift overload take a long shift amount. An int
shift amount widens to long automatically during enumerable code generation,
so a single amount type suffices for every value type; the redundant
int-valued long-amount overloads are removed. Behaviour is unchanged at the
SQL level (the result type still follows the first argument).
* SqlFunctions: change the shift amount of every leftShift/rightShift
overload from int to long, and drop the now-redundant
leftShift(int, long) / rightShift(int, long) overloads.
* BuiltInMethod: point LEFT_SHIFT/RIGHT_SHIFT at the (int, long) overloads.
* SqlOperatorTest: add executed BIGINT-by-BIGINT cases for both the
operator and function forms of left and right shift.
…veModulo helper
An earlier commit made the shift amount a long and normalized it with
Math.floorMod(y, <int width>). Math.floorMod(long, int) was only added in
JDK 9, so on JDK 8 the call resolves to Math.floorMod(long, long) (returning
long) and does not compile when assigned to an int ('incompatible types:
possible lossy conversion from long to int').
Introduce a private positiveModulo(long, int) helper that widens to
Math.floorMod(long, long) and narrows the result (always in [0, m)) back to
int, and route all shift-amount normalizations through it. This keeps the
JDK 8 build working and factors the modulo into a single named helper.
bab2663 to
0d85f00
Compare
… shifting logically
rightShift(ULong, long) shifted the raw signed long with an arithmetic '>>',
so a BIGINT UNSIGNED value with the high bit set was sign-extended. For
example, right-shifting 2^63 by 4 returned 17870283321406128128 instead of
576460752303423488. Unlike UByte/UShort/UInteger, which mask to a
non-negative domain before shifting, ULong holds the full 64 bits, so the
shift must be logical ('>>>'). leftShift(ULong, long) had the same defect in
its negative-amount branch (which shifts right); both now use '>>>'.
Also correct the reference documentation: a negative shift amount shifts in
the opposite direction by the normalized magnitude, so RIGHTSHIFT(1, -2) is
1073741824 (a left shift by 30) and LEFTSHIFT(1, -2) is 0 (a right shift by
30). The previous wording, and the LEFTSHIFT(1, -2) example, were inaccurate.
Add executing tests (run via CalciteSqlOperatorTest) for the high-bit
BIGINT UNSIGNED shifts and for the negative-amount direction, which the
existing tests did not distinguish.
|
Pushed a couple of fixes found while doing a local review of this PR: 1. 2. Negative-shift docs were inaccurate. The reference docs said negative amounts are "converted to equivalent positive shifts," and the I also added coverage for the high-bit |
| byte[] data2 = {(byte) 0x12, (byte) 0x34}; | ||
| for (int shift = -18; shift <= 18; shift++) { | ||
| byte[] result = SqlFunctions.rightShift(data2.clone(), shift); | ||
| assertEquals(2, result.length); |
There was a problem hiding this comment.
you don't really look at the result, maybe you can compute the same using integers and check?
There was a problem hiding this comment.
Done — the byte-array loops in testLeftShift/testRightShift now compute the expected result from the value's integer interpretation and assert the actual bytes, not just the length. (Doing this is what surfaced the little-endian byte order I flagged on the binary-test thread.)
| f.checkScalar("RIGHTSHIFT(0, 100)", "0", "INTEGER NOT NULL"); | ||
|
|
||
| // === Binary types === | ||
| f.checkScalar("RIGHTSHIFT(CAST(X'FF' AS BINARY(1)), 1)", "7f", "BINARY(1) NOT NULL"); |
There was a problem hiding this comment.
Is the cast necessary in these tests?
I would think x'FF' is a BINARY(1).
Is shift defined for VARBINARY?
There was a problem hiding this comment.
you will need some tests with VARBINARY too
There was a problem hiding this comment.
You're right — X'FF' is already BINARY(1) (and X'FFFF' is BINARY(2), etc.), so the cast was unnecessary. I dropped the CAST(… AS BINARY(n)) from both the >> and RIGHTSHIFT tests. Shift is defined for the whole BINARY family, so I also added VARBINARY coverage — including a pair that shows the shift width tracks the value's actual length (>> 8 is a no-op on a 1-byte value but a genuine shift on a 2-byte one).
One thing writing those tests made explicit, and I'd value your opinion on: the byte-array shift behaves as if the array were a little-endian integer (index 0 = least-significant byte). So RIGHTSHIFT(X'1234', 4) is 4103 and RIGHTSHIFT(X'FFFF', 8) is ff00, rather than the big-endian 0123 / 00ff one might expect for a binary string. This is the convention inherited from the existing LEFTSHIFT implementation and everything is self-consistent with it — but it is surprising. Do you think little-endian is the semantics we want here, or should binary shifts be big-endian? Happy to file a separate issue if it's worth revisiting.
There was a problem hiding this comment.
I think this is somewhat related to https://issues.apache.org/jira/browse/CALCITE-7368, which however treats BINARY values as big-endian.
It may be nice to have CAST(int AS BINARY(N)) << x = CAST((int << x) AS BINARY(N)), but maybe this is not possible?
I think shifts should follow the semantics of an existing database which provides this operation on BINARY values. Since we have already left shifts, isn't the semantics imposed? We should have asked this question when we implemented left shift. We should have a design before we have an implementation.
| | * | 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 based on the bit width of *value1*. For integers, this uses modulo 32; for binary types, it uses modulo (8 × byte_length). 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`. |
There was a problem hiding this comment.
this definition is not entirely clear to me for a type like VARBINARY or VARBINARY(10). Is it the dynamic length of the binary string?
There was a problem hiding this comment.
Clarified. It's 8 × N where N is the actual length in bytes of the value — for a variable-length VARBINARY that's the length of the value itself, not its declared maximum. While there I also fixed the direction wording, which only held for integer/unsigned types: for binary the shift is always in the named direction and a negative amount just folds into [0, 8 × N) by the same modulo (e.g. RIGHTSHIFT(X'F0', -4) = 0f).
|



Jira Link
CALCITE-7639
Changes Proposed
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, and the same INTEGER / BINARY / UNSIGNED_NUMERIC operand families).RIGHTSHIFT(x, n)scalar function, mirroringLEFTSHIFT.Unlike
<<, 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.The code in this PR was generated with the help of AI.