Skip to content

Fix pow() to check exponent integrality by value, not representation - #38385

Open
antiguru wants to merge 1 commit into
mainfrom
claude/cpu-214-m1zv90
Open

Fix pow() to check exponent integrality by value, not representation#38385
antiguru wants to merge 1 commit into
mainfrom
claude/cpu-214-m1zv90

Conversation

@antiguru

Copy link
Copy Markdown
Member

Motivation

The pow() function rejects negative bases with non-integral exponents to avoid complex results. However, the integrality check was examining the exponent's representation (specifically, whether exponent < 0) rather than its actual value.

This caused inconsistent behavior: the same integral value could be represented differently depending on whether it was computed inline or read back from a Row. For example, 2.0 might arrive as coefficient 2 with exponent 0, or as coefficient 20 with exponent -1 after Row encoding folds trailing zeroes into the exponent. The old check would incorrectly reject pow(-2, 2.0) in the latter case.

Description

This PR introduces is_integral_numeric() which determines whether a numeric value is integral by reducing it first, making integrality a property of the value rather than its representation. The function:

  1. Reduces the numeric (which normalizes the representation by folding trailing zeroes into the exponent)
  2. Checks if the reduced exponent is non-negative (the defining property of integral values)

The power_numeric() function now uses is_integral_numeric(b) instead of b.exponent() < 0 to validate the exponent before computing the power.

Verification

  • Added unit test power_numeric_integral_exponent_representations() that verifies the same integral exponent value (represented multiple ways: "2", "2.0", "2.000", "2E+0", "0.2E+1") all produce consistent results
  • Added sqllogictest cases that verify the fix works end-to-end when values are computed inline and then read back from a table
  • Verified that genuinely fractional exponents still correctly error
  • Verified that NaN exponents don't trip the integrality guard

https://claude.ai/code/session_01MkQaXN6bFv9JxFeHpRtof7

`power_numeric` decided that a negative base was being raised to a
non-integer power by testing `b.exponent() < 0`. That reads the
representation rather than the value: an integral exponent that still
carries trailing fractional zeroes, such as the `2.0` produced by
`1.5 + 0.5` (coefficient 20, exponent -1), tripped the complex-number
guard even though PostgreSQL returns 4 for `power(-2, 2.0)`.

This is the same hazard class as the `round(numeric, scale)` fix in
#38277. `Row` encoding reduces numerics, folding trailing zeroes into
the exponent, so equal values reach the function in different
representations depending on whether they made a round trip through a
`Row`. Whether the function errors must not depend on which one arrives:
the abstract interpreter reads its datums back out of a `Row`, so it
would call the expression infallible while the evaluator failed on the
inline representation, which is exactly the unsoundness persist filter
pushdown relies on not happening.

Reduce the exponent before reading it, so integrality is a property of
the value. `decNumberPower` itself already recognises `2.0` as an
integral exponent, so only the guard needed fixing.

Adds a `mz-expr` unit test covering several representations of the same
integral exponent, and a `numeric.slt` case that computes the exponent
at runtime from a table column so the value reaches `pow` unreduced.

Closes: CPU-214
@antiguru
antiguru requested a review from a team as a code owner August 21, 2026 08:18
@linear-code

linear-code Bot commented Aug 21, 2026

Copy link
Copy Markdown

CPU-214

@antiguru
antiguru requested review from def- and petrosagg August 21, 2026 08:28
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.

2 participants