diff --git a/composer.lock b/composer.lock index 90db21348..a709f19ea 100644 --- a/composer.lock +++ b/composer.lock @@ -2202,16 +2202,16 @@ }, { "name": "utopia-php/mongo", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "20f9a644a356599fdd6265ba48696fc42a1f1881" + "reference": "be29ee2d84b9f7efdfc6fea5b4b2d4bf95ff5ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/20f9a644a356599fdd6265ba48696fc42a1f1881", - "reference": "20f9a644a356599fdd6265ba48696fc42a1f1881", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/be29ee2d84b9f7efdfc6fea5b4b2d4bf95ff5ec4", + "reference": "be29ee2d84b9f7efdfc6fea5b4b2d4bf95ff5ec4", "shasum": "" }, "require": { @@ -2257,9 +2257,9 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.5.2" + "source": "https://github.com/utopia-php/mongo/tree/1.5.3" }, - "time": "2026-08-12T06:57:25+00:00" + "time": "2026-08-13T01:55:11+00:00" }, { "name": "utopia-php/pools", diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 2c5a65d13..7cf1b9ede 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -4934,6 +4934,77 @@ public function testSum(): void $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); } + public function testIntegersBeyondInt32(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $database->createCollection(__FUNCTION__, attributes: [ + new Document([ + '$id' => 'amount', + 'type' => Database::VAR_INTEGER, + 'size' => 8, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'amounts', + 'type' => Database::VAR_INTEGER, + 'size' => 8, + 'required' => true, + 'signed' => true, + 'array' => true, + 'filters' => [], + ]), + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], documentSecurity: false); + + // Small values encode as int32, large ones as int64. Mongo hands the + // latter back wrapped, so both widths have to appear in one row. + $database->createDocument(__FUNCTION__, new Document([ + '$id' => 'row1', + 'amount' => 2000000000, + 'amounts' => [-3408048000, -42, 3408048000, Database::MAX_BIG_INT], + ])); + $database->createDocument(__FUNCTION__, new Document([ + '$id' => 'row2', + 'amount' => 2000000000, + 'amounts' => [-42], + ])); + + foreach (['getDocument' => $database->getDocument(__FUNCTION__, 'row1'), 'find' => $database->find(__FUNCTION__, [Query::equal('$id', ['row1'])])[0]] as $path => $document) { + $this->assertIsInt($document->getAttribute('amount'), $path . ' returned a non-int scalar'); + + $amounts = $document->getAttribute('amounts'); + foreach ($amounts as $index => $amount) { + $this->assertIsInt($amount, $path . ' returned a non-int at amounts[' . $index . ']'); + } + + $this->assertSame([-3408048000, -42, 3408048000, Database::MAX_BIG_INT], $amounts); + + // An Int64 wrapper survives assertSame above but serialises as + // {"$numberLong":"..."}, which is what reaches an API client. + $this->assertSame( + '{"amount":2000000000,"amounts":[-3408048000,-42,3408048000,' . Database::MAX_BIG_INT . ']}', + \json_encode([ + 'amount' => $document->getAttribute('amount'), + 'amounts' => $amounts, + ]), + $path . ' did not serialise as plain JSON numbers' + ); + } + + // sum() declares float|int, so a total past int32 is a return type + // violation unless the adapter hands back a native integer. + $sum = $database->sum(__FUNCTION__, 'amount'); + $this->assertIsInt($sum); + $this->assertSame(4000000000, $sum); + } + public function testEncodeDecode(): void { $collection = new Document([ diff --git a/tests/e2e/Adapter/Scopes/ObjectAttributeTests.php b/tests/e2e/Adapter/Scopes/ObjectAttributeTests.php index 7c3ea359a..eb49409dc 100644 --- a/tests/e2e/Adapter/Scopes/ObjectAttributeTests.php +++ b/tests/e2e/Adapter/Scopes/ObjectAttributeTests.php @@ -964,6 +964,51 @@ public function testObjectAttributeDefaults(): void $database->deleteCollection($collectionId); } + public function testObjectAttributeIntegersBeyondInt32(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if (!$database->getAdapter()->getSupportForObject()) { + $this->markTestSkipped('Adapter does not support object attributes'); + } + + $collectionId = ID::unique(); + $database->createCollection($collectionId); + $this->createAttribute($database, $collectionId, 'meta', Database::VAR_OBJECT, 0, false); + + // An object attribute has no per-key schema, so there is no typed cast + // to lean on: whatever the adapter decodes is what reaches the client. + $database->createDocument($collectionId, new Document([ + '$id' => 'bigInts', + '$permissions' => [Permission::read(Role::any())], + 'meta' => [ + 'small' => -42, + 'count' => -3408048000, + 'nested' => ['deep' => 3408048000], + ], + ])); + + $database->purgeCachedDocument($collectionId, 'bigInts'); + $meta = $database->getDocument($collectionId, 'bigInts')->getAttribute('meta'); + + $this->assertIsInt($meta['small']); + $this->assertIsInt($meta['count']); + $this->assertIsInt($meta['nested']['deep']); + $this->assertEquals([ + 'small' => -42, + 'count' => -3408048000, + 'nested' => ['deep' => 3408048000], + ], $meta); + + // A BSON wrapper serialises as {"$numberLong":"..."}, which is what + // would reach an API client. Key order is not asserted because jsonb + // does not preserve it. + $this->assertStringNotContainsString('$numberLong', json_encode($meta, JSON_THROW_ON_ERROR)); + + $database->deleteCollection($collectionId); + } + public function testObjectAttributeEmptyObject(): void { /** @var Database $database */