Skip to content

expr: clamp round(numeric, scale) instead of overflowing - #38277

Open
def- wants to merge 1 commit into
MaterializeInc:mainfrom
def-:pr-cpu-206
Open

expr: clamp round(numeric, scale) instead of overflowing#38277
def- wants to merge 1 commit into
MaterializeInc:mainfrom
def-:pr-cpu-206

Conversation

@def-

@def- def- commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

round(a, b) right-pads with zeroes via rescale when b reaches past a's fractional digits, and otherwise shifts left by b, rounds, and shifts back. The branch condition read the exponent alone, which is wrong at both ends of its range.

It tested a_exp > 0, missing a_exp == 0, the representation of a value with neither fractional digits nor trailing zeroes. Those values took the rounding path, which shifts left by the scale first and overflows the exponent range, so round(123::numeric, 38) errored where PostgreSQL returns a value. Row encoding folds trailing zeroes into the exponent, so the result depended on which representation of an equal value reached the function. That let the abstract interpreter, which reads its datums back out of a row, call an expression infallible that the evaluator failed on, so persist filter pushdown could discard a part it has to keep. test_equivalence_ranges found it as round(extract(epoch from date '2000-01-01'), 2147483647).

Widening to a_exp >= 0 alone would swallow the special values, which report an exponent of zero as well. rescale on an infinity is an invalid operation: it yields NaN and sets invalid_operation, never the overflow the function checks, so round('Infinity'::numeric, 2) would answer NaN. That breaks the same interpreter the other direction, because NaN sorts as the maximum: a declared-monotone round mapping both ends of [-Infinity, Infinity] to NaN narrows the output to NaN alone and rules out every finite value the evaluator produces in between. That is what the second test_equivalence_ranges failure was, reported as row=Numeric(0) value=Numeric(0) spec=Within(Numeric(NaN), Numeric(NaN)). Testing finiteness keeps the specials on the rounding path, which propagates them unchanged.

Tests

test/sqllogictest/numeric.slt gains the exponent-zero case (round(extract(epoch from date '2000-01-01'), 2147483647)), the clamped-scale cases (round(123::numeric, 38), round(5::numeric, 2147483647)), and the infinity case. test/testdrive/decimal-overflow.td swaps the ROUND overflow assertion to a negative scale, which still overflows, and asserts that a positive scale no longer does.

test_equivalence_ranges is a proptest seeded from entropy, so it only failed intermittently in CI. Locally the second failure reproduced in 15 of 20 runs before the fix and in 0 of 70 after it.

Release notes

This release will fix round(<numeric>, <scale>) erroring on a scale that only right-pads a value with zeroes, and no longer turn Infinity into NaN when a scale is given.

Closes: CPU-206

@def-
def- requested review from DAlperin and antiguru August 17, 2026 21:53
@def-
def- requested a review from a team as a code owner August 17, 2026 21:53
@def-
def- marked this pull request as draft August 18, 2026 08:08
@def-
def- requested a review from petrosagg August 18, 2026 14:17
`round(a, b)` right-pads with zeroes via `rescale` when `b` reaches past `a`'s
fractional digits, and otherwise shifts left by `b`, rounds, and shifts back.
The branch condition read the exponent alone, which is wrong at both ends of
its range.

It tested `a_exp > 0`, missing `a_exp == 0`, the representation of a value with
neither fractional digits nor trailing zeroes. Those values took the rounding
path, which shifts left by the scale first and overflows the exponent range, so
`round(123::numeric, 38)` errored where PostgreSQL returns a value. Row encoding
folds trailing zeroes into the exponent, so the result depended on which
representation of an equal value reached the function. That let the abstract
interpreter, which reads its datums back out of a row, call an expression
infallible that the evaluator failed on, so persist filter pushdown could
discard a part it has to keep. `test_equivalence_ranges` found it as
`round(extract(epoch from date '2000-01-01'), 2147483647)`.

Widening to `a_exp >= 0` alone would swallow the special values, which report an
exponent of zero as well. `rescale` on an infinity is an invalid operation: it
yields `NaN` and sets `invalid_operation`, never the `overflow` the function
checks, so `round('Infinity'::numeric, 2)` would answer `NaN`. That breaks the
same interpreter the other direction, because `NaN` sorts as the maximum: a
declared-monotone `round` mapping both ends of `[-Infinity, Infinity]` to `NaN`
narrows the output to `NaN` alone and rules out every finite value the evaluator
produces in between. Testing finiteness keeps the specials on the rounding path,
which propagates them unchanged.

Extends `test/sqllogictest/numeric.slt` with the exponent-zero, clamped-scale,
and infinity cases, and `test/testdrive/decimal-overflow.td` with the positive
scale that no longer overflows.

Closes: CPU-206

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@def-
def- marked this pull request as ready for review August 19, 2026 15:27
@def-

def- commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review now. Feel free to merge in my absence or fix it up!

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.

1 participant