Skip to content

core: interval literals and types accept component values the Substrait spec disallows #1129

Description

@nielspardon

Description

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:

  1. 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.
  2. 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.
  3. 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.

🤖 Generated with AI

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcorePull requests that update java code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions