Skip to content

fix(aggregate): answer bitwise aggregates signed and refuse integer sums past int64 - #1026

Merged
abnegate merged 1 commit into
mainfrom
fix/aggregate-integer-range
Oct 16, 2026
Merged

abnegate merged 1 commit into
mainfrom
fix/aggregate-integer-range

Conversation

@abnegate

@abnegate abnegate commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

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 10 and -5: bitAnd gave 10 (correct), but bitOr and bitXor both gave 1.8446744073709552e+19 (expected -5 and -15). MySQL and MariaDB answer BIT_* as BIGINT UNSIGNED, and Aggregation::integer() turned anything past PHP_INT_MAX into a float. PostgreSQL answers signed, so the engines disagreed.

  • The MySQL and MariaDB builders now compile 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 same int.
  • SQLite has no bitwise aggregates and still refuses them with Exception\Query (Aggregate bitAnd is not supported by this adapter).
  • Mongo, Memory and Redis do not support aggregate() and refuse it with Exception\Query.

2. An integer sum past int64 came back as an inexact float. Two INT64_MAX rows summed to 1.8446744073709552e+19; the exact answer is 18446744073709551614. To match #1018/#1019, where overflow is refused rather than answered with a wrong number, an integer sum that does not fit int64 now throws Exception\Limit on every adapter. This holds in aggregate() (grouped or not) and in sum():

  • MySQL, MariaDB and PostgreSQL sum BIGINT exactly into DECIMAL/NUMERIC. Aggregation::integer() and SQL::sumOf() read those digits with BigInt::whole() and BigInt::fitsPhpInt(), so the range check is exact and never compares floats.
  • SQLite raised a raw PDOException: integer overflow (a 500). It now maps to Exception\Limit.
  • Memory and Redis wrapped the sum or warned on the int cast (sum() returned 0 with a PHP warning). They now sum with BigInt::add() and refuse a total past int64.
  • MongoDB falls back to a double when a long sum overflows. A double sum of an integer attribute is now refused.

Aggregation::integer() now has a docblock and @throws describing this. A min() or max() of an unsigned big integer past PHP_INT_MAX keeps its digits as a string, the same way a read returns it, so it is not refused by the new range check.

RaisesFloatOverflow is renamed to CompilesAggregates because it now compiles bitwise aggregates as well.

Regression tests

Test Adapters origin/main this PR
AggregationTests::testBitwiseAggregatesAreSigned64BitIntegers: positive, negative, mixed, INT64_MAX/MIN extremes, all-null and empty sets, ungrouped and grouped MySQL, MariaDB fail: 1.8446744073709552E+19 is identical to -1 pass
Postgres pass (already signed; guards consistency) pass
SQLite / Mongo / Memory / Redis pass (clean Exception\Query refusal guard) pass
AggregationTests::testIntegerAggregateSumOverflowIsRefused: above INT64_MAX, below INT64_MIN, grouped sum with one overflowing group, exact sums at INT64_MAX/MIN ungrouped and grouped MySQL, MariaDB, Postgres fail: overflow not refused pass
SQLite error: PDOException: integer overflow pass
Mongo / Memory / Redis pass (refusal guard) pass
DocumentTests::testIntegerSumOverflowIsRefused: sum() above/below int64, with max, exact at the edges MySQL, MariaDB, Postgres, Mongo fail: got 1.8446744073709552E+19 pass
SQLite error: PDOException: integer overflow pass
Memory, Redis fail: got 0 plus a PHP warning "float … is not representable as an int" pass
Unit\AggregationTest::testIntegerSumPastTheRangeOfAnIntegerIsRefused (MySQL/PG digits, MariaDB .0000, ±1 past each bound, doubles) n/a fail ×6 pass
Unit\AggregationTest::testIntegerSumAtTheEdgesOfTheRangeOfAnIntegerIsExact n/a fail (-9223372036854775808.0000 became a float) pass
Unit\AggregationTest::testBoundOfAnUnsignedBigIntegerPastTheRangeOfAnIntegerKeepsItsDigitsAsAReadDoes n/a fail (float) pass
Unit\Builder\FloatOverflowTest bitwise cases (aliased and unaliased, MySQL and MariaDB) n/a fail ×8 pass
Unit\Adapter\SumResultTest::testAnIntegerSumPastTheRangeOfAnIntegerIsRefused n/a fail ×4 pass
Unit\Validator\BigIntTest::testWholeReadsTheIntegerAWholeDecimalNames n/a new helper pass

Run 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}/rows with queries[]=bitOr("amount") (or the aggregate endpoint's equivalent) over rows amount 10 and -5 → 200, aggregate -5 (int). bitXor → -15, bitAnd → 10.
  • Same over rows INT64_MAX and INT64_MAX with sum("amount") → 400, general_query_limit-style limit error (Exception\Limit), never a float. Rows INT64_MAX-1 and 1 → 200 with 9223372036854775807 exactly.

Perf

There is one CAST per bitwise aggregate and no extra query. The overflow check reads the digits already fetched.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Integer sums now remain exact at the supported 64-bit limits and report an error when results exceed those limits, instead of returning an inaccurate value.
    • Bitwise aggregate results on MySQL and MariaDB now use signed 64-bit values for consistent results across engines.
  • Documentation
    • Clarified integer-sum overflow behavior and the signed 64-bit results returned by bitwise aggregates.

…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>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: utopia-php/database/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 880376de-4d21-4f31-a7a7-7b468fb2fec0
📥 Commits

Reviewing files that changed from the base of the PR and between 17042a6 and 69f1142.

📒 Files selected for processing: 20
  • src/Database/Adapter/Memory.php
  • src/Database/Adapter/Mongo.php
  • src/Database/Adapter/Redis.php
  • src/Database/Adapter/SQL.php
  • src/Database/Adapter/SQLite.php
  • src/Database/Aggregation.php
  • src/Database/Builder/CompilesAggregates.php
  • src/Database/Builder/MariaDB.php
  • src/Database/Builder/MySQL.php
  • src/Database/Builder/RaisesFloatOverflow.php
  • src/Database/Trait/Documents.php
  • src/Database/Validator/BigInt.php
  • tests/e2e/Adapter/Scopes/AggregationTests.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • tests/unit/Adapter/SumResultTest.php
  • tests/unit/AggregationTest.php
  • tests/unit/Builder/FloatOverflowTest.php
  • tests/unit/EmptySetAggregateContractTest.php
  • tests/unit/Joins/AggregateMinorsTest.php
  • tests/unit/Validator/BigIntTest.php
💤 Files with no reviewable changes: 1
  • src/Database/Builder/RaisesFloatOverflow.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

Integer sums now reject results outside the signed PHP integer range across adapter paths. Aggregate decoding preserves out-of-range BigInteger strings. MySQL and MariaDB compile bitwise aggregates as signed results.

Changes: Aggregate result handling

Layer / File(s) Summary
Integer aggregate decoding
src/Database/Validator/BigInt.php, src/Database/Aggregation.php, tests/unit/Validator/BigIntTest.php, tests/unit/AggregationTest.php
BigInt::whole() recognizes whole-number strings. Aggregation decoding returns in-range integer sums as integers, rejects out-of-range integer sums, and preserves out-of-range BigInteger strings. Tests cover signed boundaries and unsigned BigInteger values.
Adapter sum bounds
src/Database/Adapter/Memory.php, src/Database/Adapter/Mongo.php, src/Database/Adapter/Redis.php, src/Database/Adapter/SQL.php, src/Database/Adapter/SQLite.php, src/Database/Trait/Documents.php, tests/unit/Adapter/SumResultTest.php, tests/e2e/Adapter/Scopes/DocumentTests.php, tests/e2e/Adapter/Scopes/AggregationTests.php
Memory and Redis use separate integer and float accumulators. MongoDB checks integer attribute types before rejecting floating-point sum results. SQL and SQLite map integer overflow to LimitException. Tests cover sums at and beyond the signed boundaries. Documentation describes integer sum overflow.
Signed bitwise aggregate results
src/Database/Builder/CompilesAggregates.php, src/Database/Builder/MariaDB.php, src/Database/Builder/MySQL.php, src/Database/Builder/RaisesFloatOverflow.php, src/Database/Trait/Documents.php, tests/unit/Builder/FloatOverflowTest.php, tests/unit/EmptySetAggregateContractTest.php, tests/unit/Joins/AggregateMinorsTest.php, tests/e2e/Adapter/Scopes/AggregationTests.php
MySQL and MariaDB use CompilesAggregates to cast bitwise aggregate results to SIGNED. The trait also compiles floating aggregates with + 0 and standard-deviation aggregates through variance expressions. Tests cover signed results and empty-input behavior across engines.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 69f11

The change makes MySQL and MariaDB bitwise aggregates signed and refuses integer sums outside the int64 range. No merge-blocking risk was found.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 42.55% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 47 functions across 19 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely summarizes the two primary changes: signed bitwise aggregate results and rejection of integer sums beyond int64.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 PHPMD (2.15.0)
tests/e2e/Adapter/Scopes/DocumentTests.php

PHPMD could not process this file (exit code 255): PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in phar:///usr/bin/phpmd/vendor/pdepend/pdepend/src/main/php/PDepend/Util/Cache/Driver/FileCacheDriver.php on line 209

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@abnegate
abnegate merged commit 1ca4d03 into main Oct 16, 2026
24 checks passed
@abnegate
abnegate deleted the fix/aggregate-integer-range branch October 16, 2026 01:58
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