normalizeIntegralOffset in WindowBoundConverter returns early when the offset is not an integral literal, so the ordering expression's type is never consulted for a fractional offset. On fb6a54ad, select sum(O_SHIPPRIORITY) over (order by O_SHIPPRIORITY range between 1.5 preceding and current row) from ORDERS converts to Preceding{DecimalLiteral{1.5}} against an i32 ordering column.
Per site/docs/expressions/window_functions.md:34 at spec v0.102.0, a RANGE offset_expr's type D must be compatible with the ordering expression's type T — add(T, D) -> T and subtract(T, D) -> T must be defined. functions_arithmetic.yaml defines add/subtract only for matched pairs, and site/docs/types/type_system.md:17 rules out coercion ("Substrait employs a strict type system without any coercion rules"), so no add(i32, decimal<P,S>) -> i32 exists and the emitted plan is invalid.
The integral half of that rule is now handled: #1206 makes an integral offset that cannot be retyped to the ordering column throw. A fractional offset takes the early return above that check, so the two halves behave differently — an oversized integral offset is rejected while a type-mismatched fractional one is emitted. Calcite does not catch it either, since SqlWindow#validateFrameBoundary checks only the type family for a RANGE bound.
The existing tests precedingWithDecimalOffsetKeepsItsFraction and followingWithNonLiteralOffset both pass Optional.empty() for the ordering type, so neither exercises the retype path.
#1198 tracks the same rule going unchecked on the core side, for plans built directly or read from proto; this is the isthmus producer half for non-integral offsets.
normalizeIntegralOffsetinWindowBoundConverterreturns early when the offset is not an integral literal, so the ordering expression's type is never consulted for a fractional offset. Onfb6a54ad,select sum(O_SHIPPRIORITY) over (order by O_SHIPPRIORITY range between 1.5 preceding and current row) from ORDERSconverts toPreceding{DecimalLiteral{1.5}}against ani32ordering column.Per
site/docs/expressions/window_functions.md:34at spec v0.102.0, a RANGEoffset_expr's typeDmust be compatible with the ordering expression's typeT—add(T, D) -> Tandsubtract(T, D) -> Tmust be defined.functions_arithmetic.yamldefinesadd/subtractonly for matched pairs, andsite/docs/types/type_system.md:17rules out coercion ("Substrait employs a strict type system without any coercion rules"), so noadd(i32, decimal<P,S>) -> i32exists and the emitted plan is invalid.The integral half of that rule is now handled: #1206 makes an integral offset that cannot be retyped to the ordering column throw. A fractional offset takes the early return above that check, so the two halves behave differently — an oversized integral offset is rejected while a type-mismatched fractional one is emitted. Calcite does not catch it either, since
SqlWindow#validateFrameBoundarychecks only the type family for a RANGE bound.The existing tests
precedingWithDecimalOffsetKeepsItsFractionandfollowingWithNonLiteralOffsetboth passOptional.empty()for the ordering type, so neither exercises the retype path.#1198 tracks the same rule going unchecked on the core side, for plans built directly or read from proto; this is the isthmus producer half for non-integral offsets.