[CALCITE-7639] Support bitwise right shift (>>) operator and RIGHTSHIFT function - #5073
Conversation
bb53cb6 to
a6d5390
Compare
a6d5390 to
d765854
Compare
| // 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.
| * @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.
bab2663 to
0d85f00
Compare
|
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).
|
Agreed — design first. I dug into the current behaviour and the precedents:
So rather than cement the little-endian semantics in a new operator, I've descoped Does that split work for you? |
63ec5dd to
dc399d6
Compare
|
Yes, doing BINARY separately looks fine. |
|
what is the status of this PR? |
I dropped the Binary portion and since then it is waiting for hopefully a final round of reviews. Is there anything open from your side? |
|
You should ask for a new review when warranted, there's too much stuff going on to assume that someone scans this queue |
apologies for the misunderstanding. I was also busy with other things. I appreciate you checking in. |
mihaibudiu
left a comment
There was a problem hiding this comment.
This looks fine to me. I think you can squash so we can merge.
…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).
* `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.
dc399d6 to
f6075db
Compare
|
I squashed it. |



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.