Repository navigation
fix(aggregate): answer bitwise aggregates signed and refuse integer sums past int64 - #1026
Conversation
…ums past int64
MySQL and MariaDB answer BIT_AND, BIT_OR and BIT_XOR as BIGINT UNSIGNED, so
a result with the sign bit set (any BIT_OR or BIT_XOR touching a negative
value) read as an integer past PHP_INT_MAX and Aggregation::integer() turned
it into a lossy float: rows 10 and -5 answered bitOr 1.8446744073709552e19
where PostgreSQL answered -5. Their builders now compile each as
CAST(... AS SIGNED), which reads the same 64 bits as the two's complement
integer PostgreSQL answers, so every engine returns the same int. The cast
is done in SQL rather than in PHP so the value never leaves the engine
unsigned. SQLite has no bitwise aggregates and keeps refusing them.
MariaDB, MySQL and PostgreSQL sum integers exactly into DECIMAL or NUMERIC,
so a sum past the range of a 64-bit integer came back as an inexact float
(two INT64_MAX rows: 1.8446744073709552e19). SQLite raised a raw
PDOException ("integer overflow"), Memory and Redis wrapped or warned on
the int cast, and MongoDB returned the double it falls back to. As with a
double that overflows (#1018, #1019), an integer sum that does not fit is
refused with Exception\Limit instead of answered with a wrong number:
- Aggregation::integer() reads the engine's exact digits through
BigInt::whole() and BigInt::fitsPhpInt(), so the range check is exact,
not a float comparison, and covers a grouped sum where one group
overflows.
- SQL::sumOf() does the same for sum().
- SQLite maps "integer overflow" to Exception\Limit. SQLite also refuses a
sum whose running total overflows even when the final one fits.
- Memory and Redis sum integers with BigInt::add() and refuse a total past
int64.
- MongoDB answers an integer sum that does not fit a long as a double, so
a double sum of an integer attribute is refused.
A min() or max() of an unsigned big integer past PHP_INT_MAX keeps its
digits as a string, as a read returns it, rather than tripping the new
range check or turning into a float.
RaisesFloatOverflow becomes CompilesAggregates now that it compiles the
bitwise aggregates too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
What
Two 8.3.0 aggregate bugs found in staging QA, fixed in the library on every adapter.
1. Bitwise aggregates over negative integers came back as a lossy unsigned float on MySQL and MariaDB. Rows
10and-5:bitAndgave10(correct), butbitOrandbitXorboth gave1.8446744073709552e+19(expected-5and-15). MySQL and MariaDB answerBIT_*asBIGINT UNSIGNED, andAggregation::integer()turned anything pastPHP_INT_MAXinto a float. PostgreSQL answers signed, so the engines disagreed.CAST(BIT_x(col) AS SIGNED). This reads the same 64 bits as the two's-complement int64 PostgreSQL returns, so every engine gives the sameint.Exception\Query(Aggregate bitAnd is not supported by this adapter).aggregate()and refuse it withException\Query.2. An integer sum past int64 came back as an inexact float. Two
INT64_MAXrows summed to1.8446744073709552e+19; the exact answer is18446744073709551614. To match #1018/#1019, where overflow is refused rather than answered with a wrong number, an integersumthat does not fit int64 now throwsException\Limiton every adapter. This holds inaggregate()(grouped or not) and insum():BIGINTexactly into DECIMAL/NUMERIC.Aggregation::integer()andSQL::sumOf()read those digits withBigInt::whole()andBigInt::fitsPhpInt(), so the range check is exact and never compares floats.PDOException: integer overflow(a 500). It now maps toException\Limit.sum()returned0with a PHP warning). They now sum withBigInt::add()and refuse a total past int64.Aggregation::integer()now has a docblock and@throwsdescribing this. Amin()ormax()of an unsigned big integer pastPHP_INT_MAXkeeps its digits as a string, the same way a read returns it, so it is not refused by the new range check.RaisesFloatOverflowis renamed toCompilesAggregatesbecause it now compiles bitwise aggregates as well.Regression tests
AggregationTests::testBitwiseAggregatesAreSigned64BitIntegers: positive, negative, mixed, INT64_MAX/MIN extremes, all-null and empty sets, ungrouped and grouped1.8446744073709552E+19 is identical to -1Exception\Queryrefusal guard)AggregationTests::testIntegerAggregateSumOverflowIsRefused: above INT64_MAX, below INT64_MIN, grouped sum with one overflowing group, exact sums at INT64_MAX/MIN ungrouped and groupedPDOException: integer overflowDocumentTests::testIntegerSumOverflowIsRefused:sum()above/below int64, withmax, exact at the edges1.8446744073709552E+19PDOException: integer overflow0plus a PHP warning "float … is not representable as an int"Unit\AggregationTest::testIntegerSumPastTheRangeOfAnIntegerIsRefused(MySQL/PG digits, MariaDB.0000, ±1 past each bound, doubles)Unit\AggregationTest::testIntegerSumAtTheEdgesOfTheRangeOfAnIntegerIsExact-9223372036854775808.0000became a float)Unit\AggregationTest::testBoundOfAnUnsignedBigIntegerPastTheRangeOfAnIntegerKeepsItsDigitsAsAReadDoesUnit\Builder\FloatOverflowTestbitwise cases (aliased and unaliased, MySQL and MariaDB)Unit\Adapter\SumResultTest::testAnIntegerSumPastTheRangeOfAnIntegerIsRefusedUnit\Validator\BigIntTest::testWholeReadsTheIntegerAWholeDecimalNamesRun locally against Docker: the full adapter suites for MySQL, MariaDB, Postgres, SQLite, MongoDB, Memory and Redis pass, as do the full unit suite, Pint and PHPStan (both configs).
API repros (Appwrite)
GET /v1/tablesdb/{db}/tables/{t}/rowswithqueries[]=bitOr("amount")(or the aggregate endpoint's equivalent) over rowsamount10 and -5 →200, aggregate-5(int).bitXor→-15,bitAnd→10.INT64_MAXandINT64_MAXwithsum("amount")→400,general_query_limit-style limit error (Exception\Limit), never a float. RowsINT64_MAX-1and1→200with9223372036854775807exactly.Perf
There is one
CASTper bitwise aggregate and no extra query. The overflow check reads the digits already fetched.🤖 Generated with Claude Code
Summary by CodeRabbit