From 84a75f200fe58303862333c93be5c2235f59a493 Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Mon, 31 Aug 2026 16:36:20 +0500 Subject: [PATCH 1/7] fix(isthmus)!: reject RANGE offsets that cannot be retyped to the ordering column --- .../expression/WindowBoundConverter.java | 31 ++++++--- .../isthmus/WindowBoundConverterTest.java | 63 ++++++++++++++++++- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java index c34b3cb99..1ebe2c5b3 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java +++ b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java @@ -31,6 +31,8 @@ public class WindowBoundConverter { * @return the corresponding Substrait {@link WindowBound} * @throws IllegalStateException if the bound is not one of CURRENT ROW, UNBOUNDED, PRECEDING, or * FOLLOWING + * @throws UnsupportedOperationException if a RANGE offset's integral literal does not fit the + * ordering expression's exact type */ public static WindowBound toWindowBound( RexWindowBound rexWindowBound, @@ -45,19 +47,19 @@ public static WindowBound toWindowBound( } RexNode node = rexWindowBound.getOffset(); - Expression offset = - normalizeIntegralOffset( - node.accept(rexExpressionConverter), - isRows, - orderingType, - rexExpressionConverter.getTypeConverter()); + Expression converted = node.accept(rexExpressionConverter); // Per the spec, zero is not a valid offset; it is equivalent to CurrentRow, and producers - // should emit CurrentRow rather than a zero offset_expr. - if (integralValue(offset).filter(value -> value == 0).isPresent()) { + // should emit CurrentRow rather than a zero offset_expr. Checked before retyping: a zero + // offset needs no representation in the ordering expression's type. + if (integralValue(converted).filter(value -> value == 0).isPresent()) { return WindowBound.CURRENT_ROW; } + Expression offset = + normalizeIntegralOffset( + converted, isRows, orderingType, rexExpressionConverter.getTypeConverter()); + if (rexWindowBound.isPreceding()) { return WindowBound.Preceding.of(offset); } @@ -82,10 +84,19 @@ private static Expression normalizeIntegralOffset( // The spec requires a BOUNDS_TYPE_ROWS offset_expr to be int64. return ExpressionCreator.i64(false, value.get()); } - // BOUNDS_TYPE_RANGE: keep add(T, D) -> T defined for the ordering expression's type T. + // BOUNDS_TYPE_RANGE: an exact type match is isthmus's own policy, not a spec mandate. return orderingType .map(typeConverter::toSubstrait) - .flatMap(type -> integralLiteralOfType(type, value.get())) + .map( + type -> + integralLiteralOfType(type, value.get()) + .orElseThrow( + () -> + new UnsupportedOperationException( + "RANGE window offset " + + value.get() + + " does not fit the ordering expression's type " + + type))) .orElse(offset); } diff --git a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java index 7633adddb..18700d062 100644 --- a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java +++ b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java @@ -1,6 +1,7 @@ package io.substrait.isthmus; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import io.substrait.expression.Expression; import io.substrait.expression.ExpressionCreator; @@ -83,8 +84,8 @@ void rowsIntegralOffsetIsWidenedToI64() { @Test void rangeIntegralOffsetTakesTheOrderingExpressionType() { - // Per the spec, a RANGE offset's type D must keep add(T, D) -> T defined for the ordering - // expression's type T -- forcing it to int64 would break that for, e.g., an i32 column. + // isthmus requires a RANGE offset's type to exactly match the ordering expression's type T -- + // forcing it to int64 would break that for, e.g., an i32 column. RexNode offset = c(5, SqlTypeName.INTEGER); RexWindowBound bound = RexWindowBounds.preceding(offset); RelDataType orderingType = t(SqlTypeName.INTEGER); @@ -96,6 +97,64 @@ void rangeIntegralOffsetTakesTheOrderingExpressionType() { assertEquals(WindowBound.Preceding.of(ExpressionCreator.i32(false, 5)), converted); } + @Test + void rangeOffsetOutOfRangeForOrderingTypeThrows() { + // Calcite's SqlWindow#validateFrameBoundary only checks the bound's type family against the + // ordering type for RANGE, not its range, so an offset that doesn't fit the ordering column's + // narrower type must be rejected here rather than silently kept as the literal's own type. + RexNode offset = c(100000, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.SMALLINT); + + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + } + + @Test + void rangeOffsetExceedingDecimalPrecisionThrows() { + RexNode offset = c(12345, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.DECIMAL, 5, 2); + + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + } + + @Test + void rangeOffsetFailingFloatRoundTripThrows() { + // 16_777_217 (2^24 + 1) is the first integer a 24-bit float mantissa cannot represent exactly. + RexNode offset = c(16777217, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.REAL); + + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + } + + @Test + void rangeOffsetAgainstUnsupportedOrderingTypeThrows() { + // integralLiteralOfType has no case for a temporal ordering column, so no non-zero offset can + // ever be retyped to it. + RexNode offset = c(5, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.TIMESTAMP); + + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + } + @Test void zeroOffsetBecomesCurrentRow() { // Per the spec, zero is not a valid offset and is equivalent to CurrentRow; producers should From 31d8433583c1e41ea6fceb113c3ea2046941b1be Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Wed, 2 Sep 2026 21:43:49 +0500 Subject: [PATCH 2/7] fix(isthmus)!: reject RANGE offsets that cannot be retyped to the ordering column --- .../isthmus/WindowBoundConverterTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java index 18700d062..af129a054 100644 --- a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java +++ b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java @@ -167,4 +167,35 @@ void zeroOffsetBecomesCurrentRow() { assertEquals(WindowBound.CURRENT_ROW, converted); } + + @Test + void zeroOffsetBecomesCurrentRowEvenWhenItWouldNotFitTheDecimalOrderingType() { + // Regression test: a zero offset must short-circuit to CurrentRow before retyping is + // attempted. digitCount(0) is 1, so retyping 0 against DECIMAL(5,5) would otherwise throw + // (1 + scale(5) > precision(5)), even though zero always needs no representation at all. + RexNode offset = c(0, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.DECIMAL, 5, 5); + + WindowBound converted = + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter); + + assertEquals(WindowBound.CURRENT_ROW, converted); + } + + @Test + void zeroOffsetBecomesCurrentRowEvenAgainstAnUnsupportedOrderingType() { + // Regression test: integralLiteralOfType has no case for TIMESTAMP, so retyping a zero offset + // against it would otherwise throw, even though zero always needs no representation at all. + RexNode offset = c(0, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.TIMESTAMP); + + WindowBound converted = + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter); + + assertEquals(WindowBound.CURRENT_ROW, converted); + } } From 9c122479b45081e4f698ef40a3a11b2a7b7d2e5e Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Fri, 4 Sep 2026 18:09:28 +0500 Subject: [PATCH 3/7] Render the ordering type with StringTypeVisitor --- .../io/substrait/isthmus/expression/WindowBoundConverter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java index 1ebe2c5b3..1b55e9f7a 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java +++ b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java @@ -4,6 +4,7 @@ import io.substrait.expression.ExpressionCreator; import io.substrait.expression.WindowBound; import io.substrait.isthmus.TypeConverter; +import io.substrait.type.StringTypeVisitor; import io.substrait.type.Type; import java.math.BigDecimal; import java.math.BigInteger; @@ -96,7 +97,7 @@ private static Expression normalizeIntegralOffset( "RANGE window offset " + value.get() + " does not fit the ordering expression's type " - + type))) + + type.accept(new StringTypeVisitor())))) .orElse(offset); } From 909f1f46edbdaa9f2db7ef5288d250bcb2e4aa1c Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Fri, 4 Sep 2026 18:14:30 +0500 Subject: [PATCH 4/7] =?UTF-8?q?=20-=20Added=20message=20assertions=20to=20?= =?UTF-8?q?all=204=20existing=20assertThrows=20tests=20(checking=20both=20?= =?UTF-8?q?the=20offset=20value=20and=20a=20distinguishing=20type-name=20s?= =?UTF-8?q?ubstring,=20so=20a=20wrong-branch=20throw=20can't=20pass=20sile?= =?UTF-8?q?ntly)=20=20-=20Added=20rangeOffsetAcceptsTheOrderingTypesUpperB?= =?UTF-8?q?oundButNotBeyondIt=20=E2=80=94=20boundary=20test=20proving=20Sh?= =?UTF-8?q?ort.MAX=5FVALUE=20retypes=20cleanly=20while=20Short.MAX=5FVALUE?= =?UTF-8?q?=20+=201=20throws=20=20-=20Added=20rangeOffsetAgainstDateOrderi?= =?UTF-8?q?ngTypeThrows=20=E2=80=94=20a=20second=20"no=20such=20case"=20te?= =?UTF-8?q?st=20so=20TIMESTAMP=20isn't=20the=20only=20thing=20covering=20t?= =?UTF-8?q?hat=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../isthmus/WindowBoundConverterTest.java | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java index af129a054..c8214a191 100644 --- a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java +++ b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.substrait.expression.Expression; import io.substrait.expression.ExpressionCreator; @@ -106,11 +107,37 @@ void rangeOffsetOutOfRangeForOrderingTypeThrows() { RexWindowBound bound = RexWindowBounds.preceding(offset); RelDataType orderingType = t(SqlTypeName.SMALLINT); + UnsupportedOperationException ex = + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + assertTrue(ex.getMessage().contains("100000")); + assertTrue(ex.getMessage().contains("i16")); + } + + @Test + void rangeOffsetAcceptsTheOrderingTypesUpperBoundButNotBeyondIt() { + // The boundary this narrower-type guard actually enforces: the maximum i16 value retypes + // cleanly, but one past it throws instead of silently keeping the literal's own (wider) type. + RexWindowBound acceptedBound = + RexWindowBounds.preceding(c(Short.MAX_VALUE, SqlTypeName.INTEGER)); + RelDataType orderingType = t(SqlTypeName.SMALLINT); + + WindowBound converted = + WindowBoundConverter.toWindowBound( + acceptedBound, false, Optional.of(orderingType), rexExpressionConverter); + assertEquals( + WindowBound.Preceding.of(ExpressionCreator.i16(false, Short.MAX_VALUE)), converted); + + RexWindowBound rejectedBound = + RexWindowBounds.preceding(c(Short.MAX_VALUE + 1, SqlTypeName.INTEGER)); assertThrows( UnsupportedOperationException.class, () -> WindowBoundConverter.toWindowBound( - bound, false, Optional.of(orderingType), rexExpressionConverter)); + rejectedBound, false, Optional.of(orderingType), rexExpressionConverter)); } @Test @@ -119,11 +146,14 @@ void rangeOffsetExceedingDecimalPrecisionThrows() { RexWindowBound bound = RexWindowBounds.preceding(offset); RelDataType orderingType = t(SqlTypeName.DECIMAL, 5, 2); - assertThrows( - UnsupportedOperationException.class, - () -> - WindowBoundConverter.toWindowBound( - bound, false, Optional.of(orderingType), rexExpressionConverter)); + UnsupportedOperationException ex = + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + assertTrue(ex.getMessage().contains("12345")); + assertTrue(ex.getMessage().contains("decimal<5,2>")); } @Test @@ -133,11 +163,14 @@ void rangeOffsetFailingFloatRoundTripThrows() { RexWindowBound bound = RexWindowBounds.preceding(offset); RelDataType orderingType = t(SqlTypeName.REAL); - assertThrows( - UnsupportedOperationException.class, - () -> - WindowBoundConverter.toWindowBound( - bound, false, Optional.of(orderingType), rexExpressionConverter)); + UnsupportedOperationException ex = + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + assertTrue(ex.getMessage().contains("16777217")); + assertTrue(ex.getMessage().contains("fp32")); } @Test @@ -148,6 +181,24 @@ void rangeOffsetAgainstUnsupportedOrderingTypeThrows() { RexWindowBound bound = RexWindowBounds.preceding(offset); RelDataType orderingType = t(SqlTypeName.TIMESTAMP); + UnsupportedOperationException ex = + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + assertTrue(ex.getMessage().contains("5")); + assertTrue(ex.getMessage().contains("precision_timestamp<")); + } + + @Test + void rangeOffsetAgainstDateOrderingTypeThrows() { + // Same "no such case" path as the TIMESTAMP test above, covered separately so that path isn't + // pinned by a single ordering type. + RexNode offset = c(5, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.DATE); + assertThrows( UnsupportedOperationException.class, () -> From 3456555c759070e3b107dead31194703f9d28d6f Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Fri, 4 Sep 2026 18:25:14 +0500 Subject: [PATCH 5/7] handle negative offsets --- .../expression/WindowBoundConverter.java | 25 +++++++++++++++-- .../isthmus/WindowBoundConverterTest.java | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java index 1b55e9f7a..68ce6327b 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java +++ b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java @@ -57,14 +57,24 @@ public static WindowBound toWindowBound( return WindowBound.CURRENT_ROW; } + // The spec carries a bound's direction in the Preceding/Following choice, not in the sign of + // the offset: a negative offset is invalid, and the mirror bound with the magnitude is its + // equivalent. Calcite only rejects a negative offset for ROWS, so RANGE reaches here. + boolean preceding = rexWindowBound.isPreceding(); + Optional negative = integralValue(converted).filter(value -> value < 0); + if (negative.isPresent()) { + preceding = !preceding; + converted = negate(converted, negative.get()); + } + Expression offset = normalizeIntegralOffset( converted, isRows, orderingType, rexExpressionConverter.getTypeConverter()); - if (rexWindowBound.isPreceding()) { + if (preceding) { return WindowBound.Preceding.of(offset); } - if (rexWindowBound.isFollowing()) { + if (rexWindowBound.isFollowing() || negative.isPresent()) { return WindowBound.Following.of(offset); } @@ -72,6 +82,17 @@ public static WindowBound toWindowBound( "window bound was none of CURRENT ROW, UNBOUNDED, PRECEDING or FOLLOWING"); } + private static Expression negate(Expression offset, long value) { + return integralLiteralOfType(offset.getType(), Math.negateExact(value)) + .orElseThrow( + () -> + new UnsupportedOperationException( + "window offset " + + value + + " cannot be negated within its own type " + + offset.getType().accept(new StringTypeVisitor()))); + } + private static Expression normalizeIntegralOffset( Expression offset, boolean isRows, diff --git a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java index c8214a191..b8f5bd0a0 100644 --- a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java +++ b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java @@ -249,4 +249,32 @@ void zeroOffsetBecomesCurrentRowEvenAgainstAnUnsupportedOrderingType() { assertEquals(WindowBound.CURRENT_ROW, converted); } + + @Test + void negativePrecedingOffsetIsFlippedToFollowingWithItsMagnitude() { + // The spec carries a bound's direction in the Preceding/Following choice, not in the sign of + // the offset: RANGE BETWEEN -5 PRECEDING is equivalent to FOLLOWING 5. + RexNode offset = c(-5, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.INTEGER); + + WindowBound converted = + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter); + + assertEquals(WindowBound.Following.of(ExpressionCreator.i32(false, 5)), converted); + } + + @Test + void negativeFollowingOffsetIsFlippedToPrecedingWithItsMagnitude() { + RexNode offset = c(-5, SqlTypeName.INTEGER); + RexWindowBound bound = RexWindowBounds.following(offset); + RelDataType orderingType = t(SqlTypeName.INTEGER); + + WindowBound converted = + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter); + + assertEquals(WindowBound.Preceding.of(ExpressionCreator.i32(false, 5)), converted); + } } From 8f87ed65d197874988daffede5d5a14431771d0b Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Fri, 4 Sep 2026 18:34:20 +0500 Subject: [PATCH 6/7] fix WindowBoundConverter javadoc --- .../io/substrait/isthmus/expression/WindowBoundConverter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java index 68ce6327b..277490c76 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java +++ b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java @@ -17,7 +17,8 @@ * Utility for converting Calcite {@link RexWindowBound} to Substrait {@link WindowBound}. * *

Supports {@code CURRENT ROW}, {@code UNBOUNDED}, and {@code PRECEDING}/{@code FOLLOWING} - * bounds with an arbitrary offset expression. + * bounds with an arbitrary offset expression. A RANGE bound's integral literal offset must match + * the ordering expression's exact type. */ public class WindowBoundConverter { From a8bf3d5af5f8867251f75ea2fc1f54443fde8c59 Mon Sep 17 00:00:00 2001 From: Anas Ismail Khan Date: Mon, 7 Sep 2026 19:22:26 +0500 Subject: [PATCH 7/7] Long.MIN_VALUE now throws UnsupportedOperationException --- .../isthmus/expression/WindowBoundConverter.java | 9 ++++++++- .../isthmus/WindowBoundConverterTest.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java index 277490c76..c1d51c32e 100644 --- a/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java +++ b/isthmus/src/main/java/io/substrait/isthmus/expression/WindowBoundConverter.java @@ -84,7 +84,14 @@ public static WindowBound toWindowBound( } private static Expression negate(Expression offset, long value) { - return integralLiteralOfType(offset.getType(), Math.negateExact(value)) + long negated; + try { + negated = Math.negateExact(value); + } catch (ArithmeticException e) { + // Long.MIN_VALUE has no positive long representation. + throw new UnsupportedOperationException("window offset " + value + " cannot be negated"); + } + return integralLiteralOfType(offset.getType(), negated) .orElseThrow( () -> new UnsupportedOperationException( diff --git a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java index b8f5bd0a0..e7e6b841b 100644 --- a/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java +++ b/isthmus/src/test/java/io/substrait/isthmus/WindowBoundConverterTest.java @@ -277,4 +277,19 @@ void negativeFollowingOffsetIsFlippedToPrecedingWithItsMagnitude() { assertEquals(WindowBound.Preceding.of(ExpressionCreator.i32(false, 5)), converted); } + + @Test + void negativeOffsetOverflowingLongIsRejectedRatherThanThrowingArithmeticException() { + // Long.MIN_VALUE has no positive long representation; Math.negateExact would throw + // ArithmeticException, which toWindowBound does not document. + RexNode offset = c(Long.MIN_VALUE, SqlTypeName.BIGINT); + RexWindowBound bound = RexWindowBounds.preceding(offset); + RelDataType orderingType = t(SqlTypeName.BIGINT); + + assertThrows( + UnsupportedOperationException.class, + () -> + WindowBoundConverter.toWindowBound( + bound, false, Optional.of(orderingType), rexExpressionConverter)); + } }