You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Substrait spec constrains interval literal components, and core enforces none of it. Values the spec explicitly disallows are constructible through the POJO builders and round-trip through proto unchallenged.
From the spec's type documentation (spec v0.101.0, the version pinned in gradle/libs.versions.toml):
interval_year — "Supports a range of [-10,000..10,000] years with month precision (= [-120,000..120,000] months)", and for the literal, "int32 years and int32 months, with the added constraint that each component can never independently specify more than 10,000 years, even if the components have opposite signs". The example given as not allowed is -10000y 200000m.
interval_day<P> — "Supports a range of [-3,650,000..3,650,000] days with fractional second precision (P, number of digits) 0 <= P <= 12". The same component rule applies, with 3650001d -86400s 0us as the disallowed example. The sub-second field "carries only the fractional part of a second, expressed in 1e(-P) units", so its magnitude belongs under 1e(P).
interval_compound<P> inherits both sets of rules.
What core does today
Nothing validates any of it. Expression.IntervalDayLiteral declares bare int days(), int seconds(), long subseconds() and int precision() accessors; Expression.IntervalYearLiteral declares bare int years() and int months(); Expression.IntervalCompoundLiteral flattens both. None of the three carries a @Value.Check, and Type.java has no @Value.Check at all, so Type.IntervalDay.precision() is likewise unbounded. ProtoTypeConverter rejects only an unset interval-day precision, never an out-of-range one.
Three concrete gaps, in rough order of how easily they bite:
Per-component 10,000-year limit. The spec's own counter-examples build fine: intervalDay(false, 3650001, -86400, 0, 6) and intervalYear(false, -10000, 200000) are both accepted.
Sub-second magnitude.subseconds is not checked against 1e(P), so intervalDay(false, 0, 0, 3, 0) is accepted even though precision 0 leaves no room for a fractional part — and it converts to Calcite as 3 whole seconds, because the consumer has to interpret the field somehow.
Precision range. The spec allows 0 <= P <= 12; nothing on the type or the literal enforces either bound, so a negative precision reaches consumers. Isthmus grew its own lower-bound check for this in fix(isthmus)!: preserve interval_day precision in both conversion directions #1120, which is the sort of thing that belongs once in core rather than per-consumer.
Suggested shape
@Value.Check on each of the three literals, throwing IllegalArgumentException — not assert, which does not run in a deployed app server. The file already has the pattern: Expression.ScalarFunctionInvocation.check() delegates to a separate VariadicParameterConsistencyValidator, which keeps the rule testable on its own and out of the accessor list.
Open questions
Breaking? Adding a check means code that currently builds an out-of-range interval starts throwing. That is the point, but it is a behaviour change for any consumer relying on the current permissiveness, so it likely wants a breaking release rather than a patch.
Where to enforce. A @Value.Check fires on every construction, including the proto-to-POJO path, which is the strictest option. Validating only at the proto boundaries would let internal construction stay unchecked — probably the wrong trade, but worth stating.
Total vs component. The spec bounds each component independently and gives an overall range. A component check alone still admits 3,650,000d 86400s, whose total exceeds the day range. Both checks or just the component rule the spec spells out?
interval_compound. The spec says Substrait "gives no definition for the conversion of values between independent grains (e.g. months to days)", so a compound literal's total is not well defined and only the per-component rules can be checked.
Context
Surfaced while reviewing #1120, where scaling an interval's whole value to the declared precision overflows a long inside the spec's day range. That overflow is #1120's to fix; this issue is about core never having stated the bounds in the first place.
Description
The Substrait spec constrains interval literal components, and
coreenforces none of it. Values the spec explicitly disallows are constructible through the POJO builders and round-trip through proto unchallenged.From the spec's type documentation (spec v0.101.0, the version pinned in
gradle/libs.versions.toml):interval_year— "Supports a range of [-10,000..10,000] years with month precision (= [-120,000..120,000] months)", and for the literal, "int32years andint32months, with the added constraint that each component can never independently specify more than 10,000 years, even if the components have opposite signs". The example given as not allowed is-10000y 200000m.interval_day<P>— "Supports a range of [-3,650,000..3,650,000] days with fractional second precision (P, number of digits) 0 <= P <= 12". The same component rule applies, with3650001d -86400s 0usas the disallowed example. The sub-second field "carries only the fractional part of a second, expressed in 1e(-P) units", so its magnitude belongs under1e(P).interval_compound<P>inherits both sets of rules.What core does today
Nothing validates any of it.
Expression.IntervalDayLiteraldeclares bareint days(),int seconds(),long subseconds()andint precision()accessors;Expression.IntervalYearLiteraldeclares bareint years()andint months();Expression.IntervalCompoundLiteralflattens both. None of the three carries a@Value.Check, andType.javahas no@Value.Checkat all, soType.IntervalDay.precision()is likewise unbounded.ProtoTypeConverterrejects only an unset interval-day precision, never an out-of-range one.Three concrete gaps, in rough order of how easily they bite:
intervalDay(false, 3650001, -86400, 0, 6)andintervalYear(false, -10000, 200000)are both accepted.subsecondsis not checked against1e(P), sointervalDay(false, 0, 0, 3, 0)is accepted even though precision 0 leaves no room for a fractional part — and it converts to Calcite as 3 whole seconds, because the consumer has to interpret the field somehow.0 <= P <= 12; nothing on the type or the literal enforces either bound, so a negative precision reaches consumers. Isthmus grew its own lower-bound check for this in fix(isthmus)!: preserve interval_day precision in both conversion directions #1120, which is the sort of thing that belongs once incorerather than per-consumer.Suggested shape
@Value.Checkon each of the three literals, throwingIllegalArgumentException— notassert, which does not run in a deployed app server. The file already has the pattern:Expression.ScalarFunctionInvocation.check()delegates to a separateVariadicParameterConsistencyValidator, which keeps the rule testable on its own and out of the accessor list.Open questions
@Value.Checkfires on every construction, including the proto-to-POJO path, which is the strictest option. Validating only at the proto boundaries would let internal construction stay unchecked — probably the wrong trade, but worth stating.3,650,000d 86400s, whose total exceeds the day range. Both checks or just the component rule the spec spells out?interval_compound. The spec says Substrait "gives no definition for the conversion of values between independent grains (e.g. months to days)", so a compound literal's total is not well defined and only the per-component rules can be checked.Context
Surfaced while reviewing #1120, where scaling an interval's whole value to the declared precision overflows a
longinside the spec's day range. That overflow is #1120's to fix; this issue is aboutcorenever having stated the bounds in the first place.🤖 Generated with AI