Skip to content

Commit cb3fe2a

Browse files
abnegateclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 054ca5e commit cb3fe2a

5 files changed

Lines changed: 67 additions & 2 deletions

File tree

‎tests/e2e/Adapter/Scopes/AggregationTests.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1583,7 +1583,7 @@ public function testFloatAggregateOverflowIsRefused(): void
15831583
],
15841584
permissions: [Permission::create(Role::any()), Permission::read(Role::any())],
15851585
));
1586-
foreach ([['huge', 1e308], ['huge', 1e308], ['spread', 1e308], ['spread', -1e308], ['spread', 0.0], ['single', 1e308]] as [$series, $reading]) {
1586+
foreach ([['huge', 1e308], ['huge', 1e308], ['sunk', -1e308], ['sunk', -1e308], ['spread', 1e308], ['spread', -1e308], ['spread', 0.0], ['single', 1e308]] as [$series, $reading]) {
15871587
$database->createDocument($collection, new Document([
15881588
'series' => $series,
15891589
'reading' => $reading,
@@ -1594,6 +1594,8 @@ public function testFloatAggregateOverflowIsRefused(): void
15941594
$overflows = [
15951595
[Query::equal('series', ['huge']), Query::sum('reading', 'result')],
15961596
[Query::equal('series', ['huge']), Query::avg('reading', 'result')],
1597+
[Query::equal('series', ['sunk']), Query::sum('reading', 'result')],
1598+
[Query::equal('series', ['sunk']), Query::avg('reading', 'result')],
15971599
[Query::sum('reading', 'result'), Query::groupBy(['series'])],
15981600
];
15991601
if (! $database->getAdapter() instanceof SQLite) {

‎tests/e2e/Adapter/Scopes/DocumentTests.php‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,6 +2868,34 @@ public function testSumOverflowIsRefused(): void
28682868
$database->deleteCollection($collection);
28692869
}
28702870

2871+
public function testNegativeSumOverflowIsRefused(): void
2872+
{
2873+
$database = $this->getDatabase();
2874+
$collection = 'sum_underflow_'.\substr(\uniqid(), -6);
2875+
$database->createCollection(Collection::create(
2876+
id: $collection,
2877+
attributes: [Attribute::double(key: 'reading', required: true)],
2878+
permissions: [Permission::create(Role::any()), Permission::read(Role::any())],
2879+
));
2880+
foreach ([-1e308, -1e308] as $reading) {
2881+
$database->createDocument($collection, new Document([
2882+
'reading' => $reading,
2883+
'$permissions' => [Permission::read(Role::any())],
2884+
]));
2885+
}
2886+
2887+
foreach ([[[], null], [[], 5], [[Query::lessThan('reading', 0)], null]] as [$queries, $max]) {
2888+
try {
2889+
$database->sum($collection, 'reading', $queries, $max);
2890+
$this->fail('A sum that overflows below the range of a double is refused');
2891+
} catch (LimitException) {
2892+
$this->addToAssertionCount(1);
2893+
}
2894+
}
2895+
2896+
$database->deleteCollection($collection);
2897+
}
2898+
28712899
public function testUpdateDocument(): void
28722900
{
28732901
$document = $this->initDocumentsFixture();

‎tests/unit/Adapter/SumResultTest.php‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,24 @@ public static function sums(): iterable
2727
yield 'negative double with an exponent' => ['-1.5e300', -1.5e300];
2828
yield 'decimal past the range of an integer' => ['18446744073709551614', 18446744073709551614.0];
2929
yield 'no rows' => [null, 0];
30+
yield 'infinity' => ['INF', \INF];
31+
yield 'negative infinity' => ['-INF', -\INF];
32+
yield 'infinity in another case' => ['Inf', \INF];
33+
yield 'not a number word' => ['overflow', 0];
3034
}
3135

3236
#[DataProvider('sums')]
3337
public function testASumIsReadAsTheNumberTheEngineSent(?string $sent, int|float $expected): void
38+
{
39+
$this->assertSame($expected, $this->sumOf($sent));
40+
}
41+
42+
public function testANotANumberSumIsReadAsNan(): void
43+
{
44+
$this->assertNan($this->sumOf('NAN'));
45+
}
46+
47+
private function sumOf(?string $sent): int|float
3448
{
3549
$pdo = $this->createStub(PDO::class);
3650
$pdo->method('prepare')->willReturnCallback(function () use ($sent): PDOStatement {
@@ -47,6 +61,6 @@ public function testASumIsReadAsTheNumberTheEngineSent(?string $sent, int|float
4761
$adapter->setNamespace('namespace');
4862
$adapter->setAuthorization(new Authorization());
4963

50-
$this->assertSame($expected, $adapter->sum(Collection::create(id: 'readings'), 'reading'));
64+
return $adapter->sum(Collection::create(id: 'readings'), 'reading');
5165
}
5266
}

‎tests/unit/AggregateOverflowTest.php‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ public function testAnAggregateThatOverflowsIsRefused(bool $stringify, Query $ag
7878
$database->aggregate(self::COLLECTION, [$aggregate]);
7979
}
8080

81+
#[DataProvider('overflows')]
82+
public function testAnAggregateThatOverflowsBelowTheRangeIsRefused(bool $stringify, Query $aggregate): void
83+
{
84+
$database = $this->database($stringify, [-1e308, -1e308]);
85+
86+
$this->expectException(LimitException::class);
87+
$database->aggregate(self::COLLECTION, [$aggregate]);
88+
}
89+
8190
public function testAnAggregateThatOverflowsInOneGroupIsRefused(): void
8291
{
8392
$database = $this->database(false, [1e308, 1e308, 1.0]);

‎tests/unit/SumOverflowTest.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ public function testASumThatOverflowsIsRefused(Closure $adapter): void
8282
$database->sum(self::COLLECTION, 'reading');
8383
}
8484

85+
/**
86+
* @param Closure(): Adapter $adapter
87+
*/
88+
#[DataProvider('adapters')]
89+
public function testANegativeSumThatOverflowsIsRefused(Closure $adapter): void
90+
{
91+
$database = $this->database($adapter, [-1e308, -1e308]);
92+
93+
$this->expectException(LimitException::class);
94+
$database->sum(self::COLLECTION, 'reading');
95+
}
96+
8597
/**
8698
* @param Closure(): Adapter $adapter
8799
*/

0 commit comments

Comments
 (0)