From 1005247f22ae71ec9afae4c3ed18260318b8e1d9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 23:18:06 +1300 Subject: [PATCH] test(aggregates): cover an overflow below the range and the words sqlite sends A sum of two -1e308 rows overflows to -INF the same way two 1e308 rows overflow to INF, so sum() and aggregate() are now held to refuse it on SQLite in both fetch modes, Memory and, through the adapter E2E, every engine. The negative rows sit in their own collection so the unfiltered fast path stays order-independent. sumOf() is also held to read INF, -INF, Inf and NAN as the doubles they name and any other word as 0. Co-Authored-By: Claude Opus 5.5 --- tests/e2e/Adapter/Scopes/AggregationTests.php | 4 ++- tests/e2e/Adapter/Scopes/DocumentTests.php | 28 +++++++++++++++++++ tests/unit/Adapter/SumResultTest.php | 16 ++++++++++- tests/unit/AggregateOverflowTest.php | 9 ++++++ tests/unit/SumOverflowTest.php | 12 ++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/AggregationTests.php b/tests/e2e/Adapter/Scopes/AggregationTests.php index b9cc4578b..4dd5fc8ed 100644 --- a/tests/e2e/Adapter/Scopes/AggregationTests.php +++ b/tests/e2e/Adapter/Scopes/AggregationTests.php @@ -1583,7 +1583,7 @@ public function testFloatAggregateOverflowIsRefused(): void ], permissions: [Permission::create(Role::any()), Permission::read(Role::any())], )); - foreach ([['huge', 1e308], ['huge', 1e308], ['spread', 1e308], ['spread', -1e308], ['spread', 0.0], ['single', 1e308]] as [$series, $reading]) { + foreach ([['huge', 1e308], ['huge', 1e308], ['sunk', -1e308], ['sunk', -1e308], ['spread', 1e308], ['spread', -1e308], ['spread', 0.0], ['single', 1e308]] as [$series, $reading]) { $database->createDocument($collection, new Document([ 'series' => $series, 'reading' => $reading, @@ -1594,6 +1594,8 @@ public function testFloatAggregateOverflowIsRefused(): void $overflows = [ [Query::equal('series', ['huge']), Query::sum('reading', 'result')], [Query::equal('series', ['huge']), Query::avg('reading', 'result')], + [Query::equal('series', ['sunk']), Query::sum('reading', 'result')], + [Query::equal('series', ['sunk']), Query::avg('reading', 'result')], [Query::sum('reading', 'result'), Query::groupBy(['series'])], ]; if (! $database->getAdapter() instanceof SQLite) { diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 973550113..08b70c605 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -2868,6 +2868,34 @@ public function testSumOverflowIsRefused(): void $database->deleteCollection($collection); } + public function testNegativeSumOverflowIsRefused(): void + { + $database = $this->getDatabase(); + $collection = 'sum_underflow_'.\substr(\uniqid(), -6); + $database->createCollection(Collection::create( + id: $collection, + attributes: [Attribute::double(key: 'reading', required: true)], + permissions: [Permission::create(Role::any()), Permission::read(Role::any())], + )); + foreach ([-1e308, -1e308] as $reading) { + $database->createDocument($collection, new Document([ + 'reading' => $reading, + '$permissions' => [Permission::read(Role::any())], + ])); + } + + foreach ([[[], null], [[], 5], [[Query::lessThan('reading', 0)], null]] as [$queries, $max]) { + try { + $database->sum($collection, 'reading', $queries, $max); + $this->fail('A sum that overflows below the range of a double is refused'); + } catch (LimitException) { + $this->addToAssertionCount(1); + } + } + + $database->deleteCollection($collection); + } + public function testUpdateDocument(): void { $document = $this->initDocumentsFixture(); diff --git a/tests/unit/Adapter/SumResultTest.php b/tests/unit/Adapter/SumResultTest.php index 1de358180..f14dd3e8d 100644 --- a/tests/unit/Adapter/SumResultTest.php +++ b/tests/unit/Adapter/SumResultTest.php @@ -27,10 +27,24 @@ public static function sums(): iterable yield 'negative double with an exponent' => ['-1.5e300', -1.5e300]; yield 'decimal past the range of an integer' => ['18446744073709551614', 18446744073709551614.0]; yield 'no rows' => [null, 0]; + yield 'infinity' => ['INF', \INF]; + yield 'negative infinity' => ['-INF', -\INF]; + yield 'infinity in another case' => ['Inf', \INF]; + yield 'not a number word' => ['overflow', 0]; } #[DataProvider('sums')] public function testASumIsReadAsTheNumberTheEngineSent(?string $sent, int|float $expected): void + { + $this->assertSame($expected, $this->sumOf($sent)); + } + + public function testANotANumberSumIsReadAsNan(): void + { + $this->assertNan($this->sumOf('NAN')); + } + + private function sumOf(?string $sent): int|float { $pdo = $this->createStub(PDO::class); $pdo->method('prepare')->willReturnCallback(function () use ($sent): PDOStatement { @@ -47,6 +61,6 @@ public function testASumIsReadAsTheNumberTheEngineSent(?string $sent, int|float $adapter->setNamespace('namespace'); $adapter->setAuthorization(new Authorization()); - $this->assertSame($expected, $adapter->sum(Collection::create(id: 'readings'), 'reading')); + return $adapter->sum(Collection::create(id: 'readings'), 'reading'); } } diff --git a/tests/unit/AggregateOverflowTest.php b/tests/unit/AggregateOverflowTest.php index 299825895..e04b74645 100644 --- a/tests/unit/AggregateOverflowTest.php +++ b/tests/unit/AggregateOverflowTest.php @@ -78,6 +78,15 @@ public function testAnAggregateThatOverflowsIsRefused(bool $stringify, Query $ag $database->aggregate(self::COLLECTION, [$aggregate]); } + #[DataProvider('overflows')] + public function testAnAggregateThatOverflowsBelowTheRangeIsRefused(bool $stringify, Query $aggregate): void + { + $database = $this->database($stringify, [-1e308, -1e308]); + + $this->expectException(LimitException::class); + $database->aggregate(self::COLLECTION, [$aggregate]); + } + public function testAnAggregateThatOverflowsInOneGroupIsRefused(): void { $database = $this->database(false, [1e308, 1e308, 1.0]); diff --git a/tests/unit/SumOverflowTest.php b/tests/unit/SumOverflowTest.php index d18b74982..b9bd5f8e2 100644 --- a/tests/unit/SumOverflowTest.php +++ b/tests/unit/SumOverflowTest.php @@ -82,6 +82,18 @@ public function testASumThatOverflowsIsRefused(Closure $adapter): void $database->sum(self::COLLECTION, 'reading'); } + /** + * @param Closure(): Adapter $adapter + */ + #[DataProvider('adapters')] + public function testANegativeSumThatOverflowsIsRefused(Closure $adapter): void + { + $database = $this->database($adapter, [-1e308, -1e308]); + + $this->expectException(LimitException::class); + $database->sum(self::COLLECTION, 'reading'); + } + /** * @param Closure(): Adapter $adapter */