Skip to content

fix(spark): report interval_year overflow instead of wrapping - #1140

Merged
nielspardon merged 4 commits into
substrait-io:mainfrom
alexandrefimov:issue-1138-interval-year-overflow
Sep 4, 2026
Merged

fix(spark): report interval_year overflow instead of wrapping#1140
nielspardon merged 4 commits into
substrait-io:mainfrom
alexandrefimov:issue-1138-interval-year-overflow

Conversation

@alexandrefimov

@alexandrefimov alexandrefimov commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

ToSparkExpression.visit(IntervalYearLiteral) flattened years and months with unchecked Int arithmetic, and Spark's physical type for YearMonthInterval is a single months Int. A large year count outran that carrier silently: 178,956,971 years came out as −2,147,483,644 months, and Int.MinValue years as exactly 0 — a zero-length interval rather than a sign flip. It throws ArithmeticException now.

Only the total is significant, so the check sits on it rather than on the intermediate product: 178,956,971 years with −12 months is 2,147,483,640 months and still converts. The bound is the carrier's, not the spec's much tighter 10,000-year one, so reaching it takes a producer already far past that.

Closes #1138.

@nielspardon

Copy link
Copy Markdown
Member

I started looking at this one already. Just trying to make sense of all findings Claude produced before confronting you with them.

@nielspardon nielspardon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard the flattened total rather than the intermediate product: Math.multiplyExact(expr.years(), 12) throws on year/month pairs whose total months does fit an Int, and for interval_year the spec makes only the total significant — it treats 1001y -12000m as a legal spelling of 1y 0m. (178956971, -12), (178956971, -5) and (-178956971, 12) all have totals inside Int range and converted correctly before this change; they now throw.

The first two suggestions are one edit — the second needs the first; the rest is comment and coverage polish. Filed #1235, #1236, #1237 and #1238 for siblings I hit while reading this, the first of which is the same wrap still live in isthmus.

Comment thread spark/src/main/scala/io/substrait/spark/expression/ToSparkExpression.scala Outdated
Comment thread spark/src/test/scala/io/substrait/spark/TypesAndLiteralsSuite.scala Outdated
Comment thread spark/src/test/scala/io/substrait/spark/TypesAndLiteralsSuite.scala Outdated
Comment thread spark/src/test/scala/io/substrait/spark/TypesAndLiteralsSuite.scala Outdated
Comment thread spark/src/main/scala/io/substrait/spark/expression/ToSparkExpression.scala Outdated
years and months are both int32 on the wire and Spark's physical type
is a months Int, so nothing widens the arithmetic the way the day-time
literal is widened. Above 178,956,970 years the product wrapped, and a
large positive interval came out negative — Int.MaxValue years became
-12 months.

Far outside the spec's 10,000-year bound, so it takes a producer that
is already well past it; the point is that the two adjacent literal
conversions now take the same stance on the same arithmetic.
@alexandrefimov
alexandrefimov force-pushed the issue-1138-interval-year-overflow branch from 0b45379 to 28dee5e Compare September 3, 2026 15:31
Math.multiplyExact on the years alone refuses a mixed-sign interval whose total
still fits Spark's months Int: 178,956,971 years and -12 months is 2,147,483,640
months, inside the carrier. Only the total is significant, so widen once and
check where the value actually has to fit. The 12 comes from Util now, beside
the day-time path's own constants.

The tests cover the boundary only the addition crosses (178,956,970 years and 8
months), Int.MinValue years, which wrapped to a zero-length interval rather than
a sign flip, and the mixed-sign total that must still convert. The spec's
maximum is spelled with a nonzero months component, so deleting the months term
no longer passes.

@nielspardon nielspardon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five taken, and the check now sits where the value actually has to fit. The mutations that survived round 1 no longer do — dropping the months term, dropping the widening, or relaxing the exactness each fails the suite now, on all three variants.

Agreed on leaving both overflow messages bare: naming the value and the range on the year-month path alone would re-split the two literals on a second axis, which is the thing this PR closes. Filed #1271 for the pair.

One non-blocking suggestion left, on the type of the new constant.

Comment thread spark/src/main/scala/io/substrait/spark/utils/Util.scala Outdated
Comment thread spark/src/main/scala/io/substrait/spark/expression/ToSparkExpression.scala Outdated
The two carrier constants beside it are Long, and with this one Long the
flattened total is Long arithmetic without a .toLong at the call site. The guard
was one dropped widening away from wrapping again; the type makes that edit
inexpressible rather than caught.

@nielspardon nielspardon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, #1272 will fix the editorconfig check

@nielspardon
nielspardon merged commit 481e44f into substrait-io:main Sep 4, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spark: interval_year literal conversion silently wraps on Int overflow

2 participants