From af321ee78d1ed61b31dd25f5c797aff9072d47ef Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 16:11:19 +0530 Subject: [PATCH 01/29] Implement BIGINT support across database adapters and add getLimitForBigInt method - Added abstract method getLimitForBigInt to Adapter class. - Implemented getLimitForBigInt in Mongo, Pool, and SQL classes. - Updated MariaDB and Postgres adapters to handle VAR_BIGINT type. - Enhanced error messages for unknown types to include VAR_BIGINT. --- src/Database/Adapter.php | 7 +++++++ src/Database/Adapter/MariaDB.php | 11 +++++------ src/Database/Adapter/Mongo.php | 13 +++++++++++++ src/Database/Adapter/Pool.php | 5 +++++ src/Database/Adapter/Postgres.php | 10 ++++------ src/Database/Adapter/SQL.php | 23 ++++++++++++++++++----- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index a0c1c238a..6a9560190 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -880,6 +880,13 @@ abstract public function getLimitForString(): int; */ abstract public function getLimitForInt(): int; + /** + * Get max BIGINT limit + * + * @return int + */ + abstract public function getLimitForBigInt(): int; + /** * Get maximum attributes limit. * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 223f91e71..a3c4cad05 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1727,13 +1727,12 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554 $signed = ($signed) ? '' : ' UNSIGNED'; - - if ($size >= 8) { // INT = 4 bytes, BIGINT = 8 bytes - return 'BIGINT' . $signed; - } - return 'INT' . $signed; + case Database::VAR_BIGINT: + $signed = ($signed) ? '' : ' UNSIGNED'; + return 'BIGINT' . $signed; + case Database::VAR_FLOAT: $signed = ($signed) ? '' : ' UNSIGNED'; return 'DOUBLE' . $signed; @@ -1748,7 +1747,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return 'DATETIME(3)'; default: - throw new DatabaseException('Unknown type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_VARCHAR . ', ' . Database::VAR_TEXT . ', ' . Database::VAR_MEDIUMTEXT . ', ' . Database::VAR_LONGTEXT . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON); + throw new DatabaseException('Unknown type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_VARCHAR . ', ' . Database::VAR_TEXT . ', ' . Database::VAR_MEDIUMTEXT . ', ' . Database::VAR_LONGTEXT . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_BIGINT . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON); } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index a61a59c3a..340dca1b6 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1310,6 +1310,7 @@ public function castingAfter(Document $collection, Document $document): Document foreach ($value as &$node) { switch ($type) { case Database::VAR_INTEGER: + case Database::VAR_BIGINT: $node = (int)$node; break; case Database::VAR_DATETIME: @@ -2211,6 +2212,7 @@ private function getMongoTypeCode(string $appwriteType): string Database::VAR_MEDIUMTEXT => 'string', Database::VAR_LONGTEXT => 'string', Database::VAR_INTEGER => 'int', + Database::VAR_BIGINT => 'int', Database::VAR_FLOAT => 'double', Database::VAR_BOOLEAN => 'bool', Database::VAR_DATETIME => 'date', @@ -3004,6 +3006,17 @@ public function getLimitForInt(): int return 4294967295; } + /** + * Get max BIGINT limit + * + * @return int + */ + public function getLimitForBigInt(): int + { + // Mongo does not handle integers directly, so using MariaDB limit for now + return 18446744073709551615; + } + /** * Get maximum column limit. * Returns 0 to indicate no limit diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index e89be89ac..5c6073a36 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -328,6 +328,11 @@ public function getLimitForInt(): int return $this->delegate(__FUNCTION__, \func_get_args()); } + public function getLimitForBigInt(): int + { + return $this->delegate(__FUNCTION__, \func_get_args()); + } + public function getLimitForAttributes(): int { return $this->delegate(__FUNCTION__, \func_get_args()); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 8dcf72025..06f2b8086 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1965,13 +1965,11 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return 'TEXT'; // PostgreSQL doesn't have MEDIUMTEXT/LONGTEXT, use TEXT case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554 - - if ($size >= 8) { // INT = 4 bytes, BIGINT = 8 bytes - return 'BIGINT'; - } - return 'INTEGER'; + case Database::VAR_BIGINT: + return 'BIGINT'; + case Database::VAR_FLOAT: return 'DOUBLE PRECISION'; @@ -2000,7 +1998,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return "VECTOR({$size})"; default: - throw new DatabaseException('Unknown Type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_VARCHAR . ', ' . Database::VAR_TEXT . ', ' . Database::VAR_MEDIUMTEXT . ', ' . Database::VAR_LONGTEXT . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_OBJECT . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON); + throw new DatabaseException('Unknown Type: ' . $type . '. Must be one of ' . Database::VAR_STRING . ', ' . Database::VAR_VARCHAR . ', ' . Database::VAR_TEXT . ', ' . Database::VAR_MEDIUMTEXT . ', ' . Database::VAR_LONGTEXT . ', ' . Database::VAR_INTEGER . ', ' . Database::VAR_BIGINT . ', ' . Database::VAR_FLOAT . ', ' . Database::VAR_BOOLEAN . ', ' . Database::VAR_DATETIME . ', ' . Database::VAR_RELATIONSHIP . ', ' . Database::VAR_OBJECT . ', ' . Database::VAR_POINT . ', ' . Database::VAR_LINESTRING . ', ' . Database::VAR_POLYGON); } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 8f0bd2db2..5efbf1a17 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -896,6 +896,19 @@ public function getLimitForInt(): int return 4294967295; } + /** + * Get max BIGINT limit + * + * @return int + */ + public function getLimitForBigInt(): int + { + // 2^64 - 1 + // 18446744073709551615 is the maximum value for a 64-bit unsigned integer + // 9223372036854775807 is the maximum value for a 64-bit signed integer + return 18446744073709551615; + } + /** * Get maximum column limit. * https://mariadb.com/kb/en/innodb-limitations/#limitations-on-schema @@ -1157,11 +1170,11 @@ public function getAttributeWidth(Document $collection): int break; case Database::VAR_INTEGER: - if ($attribute['size'] >= 8) { - $total += 8; // BIGINT 8 bytes - } else { - $total += 4; // INT 4 bytes - } + $total += 4; // INT 4 bytes + break; + + case Database::VAR_BIGINT: + $total += 8; // BIGINT 8 bytes break; case Database::VAR_FLOAT: From 3137084889c458f7d1224b45cfb7083ffad65398 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 16:11:51 +0530 Subject: [PATCH 02/29] Add support for VAR_BIGINT in Database and Attribute classes - Introduced VAR_BIGINT constant in Database class. - Updated Attribute class to include maxBigIntLength parameter and handle it in validation. - Ensured backwards compatibility for existing validator construction. - Enhanced validation logic to enforce size limits for VAR_BIGINT type. --- src/Database/Database.php | 2 ++ src/Database/Validator/Attribute.php | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index ac58d72f0..84fffaf7f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -42,6 +42,7 @@ class Database // Simple Types public const VAR_STRING = 'string'; public const VAR_INTEGER = 'integer'; + public const VAR_BIGINT = 'bigint'; public const VAR_FLOAT = 'double'; public const VAR_BOOLEAN = 'boolean'; public const VAR_DATETIME = 'datetime'; @@ -2512,6 +2513,7 @@ private function validateAttribute( maxStringLength: $this->adapter->getLimitForString(), maxVarcharLength: $this->adapter->getMaxVarcharLength(), maxIntLength: $this->adapter->getLimitForInt(), + maxBigIntLength: $this->adapter->getLimitForBigInt(), supportForSchemaAttributes: $this->adapter->getSupportForSchemaAttributes(), supportForVectors: $this->adapter->getSupportForVectors(), supportForSpatialAttributes: $this->adapter->getSupportForSpatialAttributes(), diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 021a85d97..16bd52dd8 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -31,6 +31,7 @@ class Attribute extends Validator * @param int $maxStringLength * @param int $maxVarcharLength * @param int $maxIntLength + * @param int $maxBigIntLength * @param bool $supportForSchemaAttributes * @param bool $supportForVectors * @param bool $supportForSpatialAttributes @@ -49,6 +50,7 @@ public function __construct( protected int $maxStringLength = 0, protected int $maxVarcharLength = 0, protected int $maxIntLength = 0, + protected int $maxBigIntLength = 0, protected bool $supportForSchemaAttributes = false, protected bool $supportForVectors = false, protected bool $supportForSpatialAttributes = false, @@ -59,6 +61,11 @@ public function __construct( protected bool $isMigrating = false, protected bool $sharedTables = false, ) { + // Keep backwards compatibility for existing validator construction sites. + if ($this->maxBigIntLength === 0) { + $this->maxBigIntLength = $this->maxIntLength; + } + foreach ($attributes as $attribute) { $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); $this->attributes[$key] = $attribute; @@ -337,6 +344,14 @@ public function checkType(Document $attribute): bool } break; + case Database::VAR_BIGINT: + $limit = ($signed) ? $this->maxBigIntLength / 2 : $this->maxBigIntLength; + if ($size > $limit) { + $this->message = 'Max size allowed for bigint is: ' . number_format($limit); + throw new DatabaseException($this->message); + } + break; + case Database::VAR_FLOAT: case Database::VAR_BOOLEAN: case Database::VAR_DATETIME: From b5c74471c907f336ac547f63c02d639ae4c8b4b1 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 16:12:05 +0530 Subject: [PATCH 03/29] Add tests for BIGINT attribute handling and validation limits - Introduced testCreateAttributesBigIntSizeLimit to verify exception handling for oversized BIGINT attributes. - Added testCreateDocumentWithBigIntType to ensure correct document creation and retrieval with BIGINT attributes. - Implemented testBigIntSizeTooLarge and testUnsignedBigIntSizeLimit to validate size constraints in the Attribute validator. --- tests/e2e/Adapter/Scopes/AttributeTests.php | 29 ++++++++++++ tests/e2e/Adapter/Scopes/DocumentTests.php | 28 +++++++++++ tests/unit/Validator/AttributeTest.php | 52 +++++++++++++++++++++ 3 files changed, 109 insertions(+) diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index bf376d101..4f5d6361c 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2221,6 +2221,35 @@ public function testCreateAttributesIntegerSizeLimit(): void } } + public function testCreateAttributesBigIntSizeLimit(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + if (!$database->getAdapter()->getSupportForBatchCreateAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $database->createCollection(__FUNCTION__); + + $limit = $database->getAdapter()->getLimitForBigInt() / 2; + + $attributes = [[ + '$id' => 'foo', + 'type' => Database::VAR_BIGINT, + 'size' => (int)$limit + 1, + 'required' => false + ]]; + + try { + $database->createAttributes(__FUNCTION__, $attributes); + $this->fail('Expected DatabaseException not thrown'); + } catch (\Throwable $e) { + $this->assertInstanceOf(DatabaseException::class, $e); + } + } + public function testCreateAttributesSuccessMultiple(): void { /** @var Database $database */ diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index d16004d32..90184fc8d 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -103,6 +103,34 @@ public function testBigintSequence(): void } } + public function testCreateDocumentWithBigIntType(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $database->createCollection(__FUNCTION__); + $this->assertEquals(true, $database->createAttribute(__FUNCTION__, 'bigint_signed', Database::VAR_BIGINT, 0, true)); + $this->assertEquals(true, $database->createAttribute(__FUNCTION__, 'bigint_unsigned', Database::VAR_BIGINT, 0, true, signed: false)); + + $document = $database->createDocument(__FUNCTION__, new Document([ + '$id' => 'bigint-type-doc', + '$permissions' => [Permission::read(Role::any())], + 'bigint_signed' => -Database::MAX_BIG_INT, + 'bigint_unsigned' => Database::MAX_BIG_INT, + ])); + + $this->assertIsInt($document->getAttribute('bigint_signed')); + $this->assertEquals(-Database::MAX_BIG_INT, $document->getAttribute('bigint_signed')); + $this->assertIsInt($document->getAttribute('bigint_unsigned')); + $this->assertEquals(Database::MAX_BIG_INT, $document->getAttribute('bigint_unsigned')); + + $results = $database->find(__FUNCTION__, [ + Query::equal('bigint_unsigned', [Database::MAX_BIG_INT]) + ]); + $this->assertCount(1, $results); + $this->assertEquals('bigint-type-doc', $results[0]->getId()); + } + public function testCreateDocument(): Document { /** @var Database $database */ diff --git a/tests/unit/Validator/AttributeTest.php b/tests/unit/Validator/AttributeTest.php index 2f7303cd1..2fba4441f 100644 --- a/tests/unit/Validator/AttributeTest.php +++ b/tests/unit/Validator/AttributeTest.php @@ -1139,6 +1139,58 @@ public function testUnsignedIntegerSizeTooLarge(): void $validator->isValid($attribute); } + public function testBigIntSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + maxBigIntLength: 200, + ); + + $attribute = new Document([ + '$id' => ID::custom('counter'), + 'key' => 'counter', + 'type' => Database::VAR_BIGINT, + 'size' => 101, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for bigint is: 100'); + $validator->isValid($attribute); + } + + public function testUnsignedBigIntSizeLimit(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + maxBigIntLength: 200, + ); + + $attribute = new Document([ + '$id' => ID::custom('counter'), + 'key' => 'counter', + 'type' => Database::VAR_BIGINT, + 'size' => 200, + 'required' => false, + 'default' => null, + 'signed' => false, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + public function testDuplicateAttributeIdCaseInsensitive(): void { $validator = new Attribute( From bd14576c2f387a676f8e11411fc62d73e312d025 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 17:56:33 +0530 Subject: [PATCH 04/29] Update getLimitForBigInt method to return 4294967295 for Mongo and SQL adapters - Adjusted the getLimitForBigInt method in both Mongo and SQL classes to return 4294967295, reflecting the maximum value for a 32-bit unsigned integer. - Updated comments to clarify the handling of integer limits in PHP. --- src/Database/Adapter/Mongo.php | 2 +- src/Database/Adapter/SQL.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 340dca1b6..509316bdd 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -3014,7 +3014,7 @@ public function getLimitForInt(): int public function getLimitForBigInt(): int { // Mongo does not handle integers directly, so using MariaDB limit for now - return 18446744073709551615; + return 4294967295; } /** diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 5efbf1a17..f767c7fe2 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -906,7 +906,8 @@ public function getLimitForBigInt(): int // 2^64 - 1 // 18446744073709551615 is the maximum value for a 64-bit unsigned integer // 9223372036854775807 is the maximum value for a 64-bit signed integer - return 18446744073709551615; + // in php we can't represent 64-bit integer, so greater than 4294967295 will be treated as bigint + return 4294967295; } /** From acfeae1aa37f29bb84381eec5c7d70a3565ea4ae Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 18:00:44 +0530 Subject: [PATCH 05/29] updated bigint mongo --- src/Database/Adapter/Mongo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 509316bdd..4596f4027 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2212,7 +2212,7 @@ private function getMongoTypeCode(string $appwriteType): string Database::VAR_MEDIUMTEXT => 'string', Database::VAR_LONGTEXT => 'string', Database::VAR_INTEGER => 'int', - Database::VAR_BIGINT => 'int', + Database::VAR_BIGINT => 'long', Database::VAR_FLOAT => 'double', Database::VAR_BOOLEAN => 'bool', Database::VAR_DATETIME => 'date', From 07358a5c2f6b7d50ba284700900ec6c1416a9ba4 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 18:06:27 +0530 Subject: [PATCH 06/29] Enhance BIGINT handling in MariaDB and SQL adapters - Updated MariaDB adapter to return 'BIGINT' for sizes greater than or equal to 8 bytes. - Modified SQL adapter to correctly calculate total size for VAR_INTEGER based on the attribute size, distinguishing between INT and BIGINT. --- src/Database/Adapter/MariaDB.php | 5 +++++ src/Database/Adapter/SQL.php | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index a3c4cad05..0faaa8622 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1727,6 +1727,11 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554 $signed = ($signed) ? '' : ' UNSIGNED'; + + if ($size >= 8) { // INT = 4 bytes, BIGINT = 8 bytes + return 'BIGINT' . $signed; + } + return 'INT' . $signed; case Database::VAR_BIGINT: diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index f767c7fe2..a64dbec39 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1171,7 +1171,11 @@ public function getAttributeWidth(Document $collection): int break; case Database::VAR_INTEGER: - $total += 4; // INT 4 bytes + if ($attribute['size'] >= 8) { + $total += 8; // BIGINT 8 bytes + } else { + $total += 4; // INT 4 bytes + } break; case Database::VAR_BIGINT: From 909d8a502dc7e7050b7cf18e974004d7ce4cf6bd Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 18:13:50 +0530 Subject: [PATCH 07/29] Add VAR_BIGINT to Database class attribute types - Included VAR_BIGINT in the list of attribute types in the Database class. - Ensured consistency in handling BIGINT attributes across the database implementation. --- src/Database/Database.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 84fffaf7f..e96b9a479 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2609,6 +2609,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void self::VAR_MEDIUMTEXT, self::VAR_LONGTEXT, self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT, self::VAR_BOOLEAN, self::VAR_DATETIME, @@ -2977,6 +2978,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = self::VAR_MEDIUMTEXT, self::VAR_LONGTEXT, self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT, self::VAR_BOOLEAN, self::VAR_DATETIME, From f2acd8c0787955f72f70ac675ff2d953363b452f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 18:18:14 +0530 Subject: [PATCH 08/29] Enhance BIGINT size limit validation in Database class - Added handling for VAR_BIGINT in the size limit validation logic. - Implemented exception throwing for oversized BIGINT attributes to ensure proper error handling. --- src/Database/Database.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index e96b9a479..5aa61d4e3 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2584,6 +2584,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void } break; case self::VAR_INTEGER: + case self::VAR_BIGINT: case self::VAR_FLOAT: case self::VAR_BOOLEAN: if ($type !== $defaultType) { @@ -2912,6 +2913,12 @@ public function updateAttribute(string $collection, string $id, ?string $type = throw new DatabaseException('Max size allowed for int is: ' . number_format($limit)); } break; + case self::VAR_BIGINT: + $limit = ($signed) ? $this->adapter->getLimitForBigInt() / 2 : $this->adapter->getLimitForBigInt(); + if ($size > $limit) { + throw new DatabaseException('Max size allowed for bigint is: ' . number_format($limit)); + } + break; case self::VAR_FLOAT: case self::VAR_BOOLEAN: case self::VAR_DATETIME: From 602eefcf8ea251b239ae84bce3ce4284d7aecade Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 18:19:53 +0530 Subject: [PATCH 09/29] Refine BIGINT handling in Structure and Filter validators - Updated validation logic to correctly determine bit size for VAR_BIGINT and VAR_INTEGER based on attribute size. - Ensured consistent handling of signed and unsigned integers across both validators. --- src/Database/Validator/Query/Filter.php | 3 ++- src/Database/Validator/Structure.php | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index dd07e44c8..83f40fba6 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -153,9 +153,10 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s break; case Database::VAR_INTEGER: + case Database::VAR_BIGINT: $size = $attributeSchema['size'] ?? 4; $signed = $attributeSchema['signed'] ?? true; - $bits = $size >= 8 ? 64 : 32; + $bits = ($attributeType === Database::VAR_BIGINT || $size >= 8) ? 64 : 32; // For 64-bit unsigned, use signed since PHP doesn't support true 64-bit unsigned $unsigned = !$signed && $bits < 64; $validator = new Integer(false, $bits, $unsigned); diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index a65734dbd..09c4efd29 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -352,13 +352,15 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) break; case Database::VAR_INTEGER: + case Database::VAR_BIGINT: // Determine bit size based on attribute size in bytes - $bits = $size >= 8 ? 64 : 32; + // BIGINT is always 64-bit in SQL adapters; VAR_INTEGER uses size to decide. + $bits = ($type === Database::VAR_BIGINT || $size >= 8) ? 64 : 32; // For 64-bit unsigned, use signed since PHP doesn't support true 64-bit unsigned // The Range validator will restrict to positive values only $unsigned = !$signed && $bits < 64; $validators[] = new Integer(false, $bits, $unsigned); - $max = $size >= 8 ? Database::MAX_BIG_INT : Database::MAX_INT; + $max = $bits === 64 ? Database::MAX_BIG_INT : Database::MAX_INT; $min = $signed ? -$max : 0; $validators[] = new Range($min, $max, Database::VAR_INTEGER); break; From 14828d9dae2503fa73d1bbfa7a54efb3319755e3 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 27 Mar 2026 18:32:38 +0530 Subject: [PATCH 10/29] Refine BIGINT type determination in Postgres adapter - Updated Postgres adapter to return 'BIGINT' for sizes greater than or equal to 8 bytes in the type determination logic for VAR_INTEGER. - Ensured consistent handling of integer types across the database implementation. --- src/Database/Adapter/Postgres.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 06f2b8086..7362dc265 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1965,6 +1965,11 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return 'TEXT'; // PostgreSQL doesn't have MEDIUMTEXT/LONGTEXT, use TEXT case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554 + + if ($size >= 8) { // INT = 4 bytes, BIGINT = 8 bytes + return 'BIGINT'; + } + return 'INTEGER'; case Database::VAR_BIGINT: From 080de0e82e30a48c7368e6180bb58a88a62dded1 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 11:37:06 +0530 Subject: [PATCH 11/29] Remove BIGINT size limit validation from Database and Validator classes - Eliminated size limit checks for VAR_BIGINT in the Database and Attribute classes to allow larger values. - Updated Mongo and SQL adapters to return a constant for BIGINT limits, ensuring consistent handling across implementations. - Refactored tests to reflect the removal of size constraints and validate successful attribute creation without exceptions. --- src/Database/Adapter/Mongo.php | 3 +-- src/Database/Adapter/SQL.php | 6 +----- src/Database/Database.php | 4 ---- src/Database/Validator/Attribute.php | 5 ----- tests/e2e/Adapter/Scopes/AttributeTests.php | 20 ++++++++++++-------- tests/unit/Validator/AttributeTest.php | 6 ++---- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 4596f4027..84fbaf80a 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -3013,8 +3013,7 @@ public function getLimitForInt(): int */ public function getLimitForBigInt(): int { - // Mongo does not handle integers directly, so using MariaDB limit for now - return 4294967295; + return Database::MAX_BIG_INT; } /** diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index a64dbec39..199cfcce0 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -903,11 +903,7 @@ public function getLimitForInt(): int */ public function getLimitForBigInt(): int { - // 2^64 - 1 - // 18446744073709551615 is the maximum value for a 64-bit unsigned integer - // 9223372036854775807 is the maximum value for a 64-bit signed integer - // in php we can't represent 64-bit integer, so greater than 4294967295 will be treated as bigint - return 4294967295; + return Database::MAX_BIG_INT; } /** diff --git a/src/Database/Database.php b/src/Database/Database.php index 5aa61d4e3..a2c8ca7c2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2914,10 +2914,6 @@ public function updateAttribute(string $collection, string $id, ?string $type = } break; case self::VAR_BIGINT: - $limit = ($signed) ? $this->adapter->getLimitForBigInt() / 2 : $this->adapter->getLimitForBigInt(); - if ($size > $limit) { - throw new DatabaseException('Max size allowed for bigint is: ' . number_format($limit)); - } break; case self::VAR_FLOAT: case self::VAR_BOOLEAN: diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 16bd52dd8..83e1f7174 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -345,11 +345,6 @@ public function checkType(Document $attribute): bool break; case Database::VAR_BIGINT: - $limit = ($signed) ? $this->maxBigIntLength / 2 : $this->maxBigIntLength; - if ($size > $limit) { - $this->message = 'Max size allowed for bigint is: ' . number_format($limit); - throw new DatabaseException($this->message); - } break; case Database::VAR_FLOAT: diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 4f5d6361c..7543fad73 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2221,7 +2221,8 @@ public function testCreateAttributesIntegerSizeLimit(): void } } - public function testCreateAttributesBigIntSizeLimit(): void + + public function testCreateAttributesBigIntIgnoresSizeLimit(): void { /** @var Database $database */ $database = $this->getDatabase(); @@ -2234,20 +2235,23 @@ public function testCreateAttributesBigIntSizeLimit(): void $database->createCollection(__FUNCTION__); $limit = $database->getAdapter()->getLimitForBigInt() / 2; + $size = (int)$limit + 1; $attributes = [[ '$id' => 'foo', 'type' => Database::VAR_BIGINT, - 'size' => (int)$limit + 1, + 'size' => $size, 'required' => false ]]; - try { - $database->createAttributes(__FUNCTION__, $attributes); - $this->fail('Expected DatabaseException not thrown'); - } catch (\Throwable $e) { - $this->assertInstanceOf(DatabaseException::class, $e); - } + $result = $database->createAttributes(__FUNCTION__, $attributes); + $this->assertTrue($result); + + $collection = $database->getCollection(__FUNCTION__); + $attrs = $collection->getAttribute('attributes'); + $this->assertCount(1, $attrs); + $this->assertEquals('foo', $attrs[0]['$id']); + $this->assertEquals($size, $attrs[0]['size']); } public function testCreateAttributesSuccessMultiple(): void diff --git a/tests/unit/Validator/AttributeTest.php b/tests/unit/Validator/AttributeTest.php index 2fba4441f..fd82c1fd8 100644 --- a/tests/unit/Validator/AttributeTest.php +++ b/tests/unit/Validator/AttributeTest.php @@ -1139,7 +1139,7 @@ public function testUnsignedIntegerSizeTooLarge(): void $validator->isValid($attribute); } - public function testBigIntSizeTooLarge(): void + public function testBigIntSizeNotLimited(): void { $validator = new Attribute( attributes: [], @@ -1161,9 +1161,7 @@ public function testBigIntSizeTooLarge(): void 'filters' => [], ]); - $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Max size allowed for bigint is: 100'); - $validator->isValid($attribute); + $this->assertTrue($validator->isValid($attribute)); } public function testUnsignedBigIntSizeLimit(): void From 73e917e57f287778d78694308252f5ad05de8bdd Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 11:43:36 +0530 Subject: [PATCH 12/29] Add support for VAR_BIGINT in Database class and corresponding tests - Included VAR_BIGINT in the whitelist for attribute types in the Database class. - Implemented a new test to validate the creation, updating, and atomic operations for BIGINT attributes in the DocumentTests. - Ensured that the new tests confirm correct behavior for incrementing and decrementing BIGINT values. --- src/Database/Database.php | 2 ++ tests/e2e/Adapter/Scopes/DocumentTests.php | 39 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index a2c8ca7c2..77246d884 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -7430,6 +7430,7 @@ public function increaseDocumentAttribute( $whiteList = [ self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT ]; @@ -7528,6 +7529,7 @@ public function decreaseDocumentAttribute( $whiteList = [ self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT ]; diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 90184fc8d..5567ce844 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -1495,6 +1495,45 @@ public function testIncreaseDecrease(): Document return $document; } + public function testCreateUpdateBigIntAndIncrementDecrement(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $collection = 'bigint_update_increase_decrease'; + $database->createCollection($collection); + + $this->assertEquals(true, $database->createAttribute($collection, 'inc', Database::VAR_BIGINT, 8, true)); + $this->assertEquals(true, $database->createAttribute($collection, 'dec', Database::VAR_BIGINT, 8, true)); + + $document = $database->createDocument($collection, new Document([ + 'inc' => 10, + 'dec' => 10, + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ] + ])); + + $this->assertIsInt($document->getAttribute('inc')); + $this->assertEquals(10, $document->getAttribute('inc')); + + // Verify regular update works for bigint attributes + $updated = $database->updateDocument($collection, $document->getId(), new Document([ + 'inc' => 20, + ])); + $this->assertEquals(20, $updated->getAttribute('inc')); + + // Verify atomic increment/decrement supports bigint schema attributes + $afterInc = $database->increaseDocumentAttribute($collection, $document->getId(), 'inc', 5, 30); + $this->assertEquals(25, $afterInc->getAttribute('inc')); + + $afterDec = $database->decreaseDocumentAttribute($collection, $document->getId(), 'dec', 3, 7); + $this->assertEquals(7, $afterDec->getAttribute('dec')); + } + /** * @depends testIncreaseDecrease */ From ef5c77972a95129eb929cce6c8f7c674db930ec9 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 12:01:05 +0530 Subject: [PATCH 13/29] updated tests --- src/Database/Database.php | 3 +++ tests/e2e/Adapter/Scopes/AttributeTests.php | 2 +- tests/e2e/Adapter/Scopes/DocumentTests.php | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 77246d884..0e15f26f8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -8941,6 +8941,9 @@ public function casting(Document $collection, Document $document): Document case self::VAR_INTEGER: $node = (int)$node; break; + case self::VAR_BIGINT: + $node = (int)$node; + break; case self::VAR_FLOAT: $node = (float)$node; break; diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 7543fad73..3c5633734 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2222,7 +2222,7 @@ public function testCreateAttributesIntegerSizeLimit(): void } - public function testCreateAttributesBigIntIgnoresSizeLimit(): void + public function testCreateAttributesBigInt(): void { /** @var Database $database */ $database = $this->getDatabase(); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 5567ce844..673f57409 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -1522,7 +1522,7 @@ public function testCreateUpdateBigIntAndIncrementDecrement(): void // Verify regular update works for bigint attributes $updated = $database->updateDocument($collection, $document->getId(), new Document([ - 'inc' => 20, + 'inc' => 20 ])); $this->assertEquals(20, $updated->getAttribute('inc')); From 4490a49ac3f451386ba65c473afe25d36394fdb1 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 19:45:12 +0530 Subject: [PATCH 14/29] big int validator for attribute --- src/Database/Validator/Attribute.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 83e1f7174..c2f0f54df 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -430,6 +430,7 @@ public function checkType(Document $attribute): bool Database::VAR_MEDIUMTEXT, Database::VAR_LONGTEXT, Database::VAR_INTEGER, + Database::VAR_BIGINT, Database::VAR_FLOAT, Database::VAR_BOOLEAN, Database::VAR_DATETIME, From 4c5aeb1ff936db19acfd93add8a86efc397a43ab Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 19:48:18 +0530 Subject: [PATCH 15/29] updated --- src/Database/Validator/Attribute.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index c2f0f54df..97fdb4c3e 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -431,6 +431,7 @@ public function checkType(Document $attribute): bool Database::VAR_LONGTEXT, Database::VAR_INTEGER, Database::VAR_BIGINT, + Database::VAR_BIGINT, Database::VAR_FLOAT, Database::VAR_BOOLEAN, Database::VAR_DATETIME, @@ -526,6 +527,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void } break; case Database::VAR_INTEGER: + case Database::VAR_BIGINT: case Database::VAR_FLOAT: case Database::VAR_BOOLEAN: if ($type !== $defaultType) { @@ -554,6 +556,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void Database::VAR_MEDIUMTEXT, Database::VAR_LONGTEXT, Database::VAR_INTEGER, + Database::VAR_BIGINT, Database::VAR_FLOAT, Database::VAR_BOOLEAN, Database::VAR_DATETIME, From 8137486414bd19f73e5ef93e3a160269596b8b57 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 19:56:32 +0530 Subject: [PATCH 16/29] updated attribute validator --- src/Database/Validator/Attribute.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 97fdb4c3e..a924f189b 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -527,7 +527,6 @@ protected function validateDefaultTypes(string $type, mixed $default): void } break; case Database::VAR_INTEGER: - case Database::VAR_BIGINT: case Database::VAR_FLOAT: case Database::VAR_BOOLEAN: if ($type !== $defaultType) { @@ -535,6 +534,12 @@ protected function validateDefaultTypes(string $type, mixed $default): void throw new DatabaseException($this->message); } break; + case Database::VAR_BIGINT: + if ($defaultType !== 'integer') { + $this->message = 'Default value ' . $default . ' does not match given type ' . $type; + throw new DatabaseException($this->message); + } + break; case Database::VAR_DATETIME: if ($defaultType !== Database::VAR_STRING) { $this->message = 'Default value ' . $default . ' does not match given type ' . $type; From b87cf3a9727f24cf080d8d2c21f376b88b308668 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 30 Mar 2026 20:06:08 +0530 Subject: [PATCH 17/29] updated --- src/Database/Database.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 0e15f26f8..44ea8013b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2584,13 +2584,17 @@ protected function validateDefaultTypes(string $type, mixed $default): void } break; case self::VAR_INTEGER: - case self::VAR_BIGINT: case self::VAR_FLOAT: case self::VAR_BOOLEAN: if ($type !== $defaultType) { throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type); } break; + case Database::VAR_BIGINT: + if ($defaultType !== 'integer') { + throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type); + } + break; case self::VAR_DATETIME: if ($defaultType !== self::VAR_STRING) { throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type); From 5ee5d1b8453815550051cec657e64d061aedaf3d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 16:49:58 +0530 Subject: [PATCH 18/29] Add support for unsigned big integers in database adapters and validators - Introduced `getSupportForUnsignedBigInt` method in `Adapter`, `MariaDB`, and `SQLite` classes to indicate support for unsigned big integers. - Implemented normalization of attribute sizes for `bigint` types in the `Database` class. - Added `BigInt` helper class for managing big integer validation and formatting. - Updated `Attribute`, `Structure`, and `Filter` validators to handle signed and unsigned big integers correctly. - Enhanced tests to cover scenarios for creating and validating attributes with big integers, including edge cases for signed and unsigned values. --- src/Database/Adapter.php | 5 + src/Database/Adapter/MariaDB.php | 5 + src/Database/Adapter/SQLite.php | 5 + src/Database/Database.php | 84 ++++++++-- src/Database/Helpers/BigInt.php | 93 +++++++++++ src/Database/Validator/Attribute.php | 17 ++ src/Database/Validator/BigInt.php | 51 ++++++ src/Database/Validator/PartialStructure.php | 2 +- src/Database/Validator/Queries/Documents.php | 6 +- src/Database/Validator/Query/Filter.php | 10 +- src/Database/Validator/Structure.php | 27 +++- tests/e2e/Adapter/Scopes/AttributeTests.php | 84 +++++++++- tests/e2e/Adapter/Scopes/DocumentTests.php | 154 ++++++++++++++++++- tests/unit/Validator/Query/FilterTest.php | 16 ++ tests/unit/Validator/StructureTest.php | 118 ++++++++++++++ 15 files changed, 649 insertions(+), 28 deletions(-) create mode 100644 src/Database/Helpers/BigInt.php create mode 100644 src/Database/Validator/BigInt.php diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 39b96d7fb..6e5dec54b 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1449,6 +1449,11 @@ abstract public function decodeLinestring(string $wkb): array; */ abstract public function decodePolygon(string $wkb): array; + public function getSupportForUnsignedBigInt(): bool + { + return false; + } + /** * Returns the document after casting * @param Document $collection diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0faaa8622..e45e9f6ac 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2255,6 +2255,11 @@ public function getSupportForObject(): bool return false; } + public function getSupportForUnsignedBigInt(): bool + { + return true; + } + /** * Are object (JSON) indexes supported? * diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 3c25987eb..59f173278 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1019,6 +1019,11 @@ public function getSupportForObject(): bool return false; } + public function getSupportForUnsignedBigInt(): bool + { + return false; + } + /** * Are object (JSON) indexes supported? * diff --git a/src/Database/Database.php b/src/Database/Database.php index 44ea8013b..0fd9a3333 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -22,6 +22,7 @@ use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Type as TypeException; +use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -1905,6 +1906,7 @@ public function updateCollection(string $id, array $permissions, bool $documentS public function getCollection(string $id): Document { $collection = $this->silent(fn () => $this->getDocument(self::METADATA, $id)); + $collection = $this->normalizeCollectionAttributeSizes($collection); if ( $id !== self::METADATA @@ -1940,6 +1942,8 @@ public function listCollections(int $limit = 25, int $offset = 0): array Query::offset($offset) ])); + $result = \array_map(fn (Document $collection) => $this->normalizeCollectionAttributeSizes($collection), $result); + try { $this->trigger(self::EVENT_COLLECTION_LIST, $result); } catch (\Throwable $e) { @@ -1949,6 +1953,40 @@ public function listCollections(int $limit = 25, int $offset = 0): array return $result; } + private function normalizeCollectionAttributeSizes(Document $collection): Document + { + if ($collection->isEmpty()) { + return $collection; + } + + $attributes = $collection->getAttribute('attributes', []); + if (!\is_array($attributes)) { + return $collection; + } + + foreach ($attributes as $attribute) { + if (!$attribute instanceof Document) { + continue; + } + + $type = $attribute->getAttribute('type'); + $size = $attribute->getAttribute('size', 0); + + if ($type === self::VAR_BIGINT) { + // Keep bigint size as string to avoid precision loss. + $attribute->setAttribute('size', (string)$size); + continue; + } + + // Keep legacy behavior for non-bigint metadata. + $attribute->setAttribute('size', (int)$size); + } + + $collection->setAttribute('attributes', $attributes); + + return $collection; + } + /** * Get Collection Size * @@ -2193,7 +2231,7 @@ public function createAttribute(string $collection, string $id, string $type, in '$id' => ID::custom($id), 'key' => $id, 'type' => $type, - 'size' => $size, + 'size' => $type === self::VAR_BIGINT ? (string)$size : $size, 'required' => $required, 'default' => $default, 'signed' => $signed, @@ -2490,7 +2528,7 @@ private function validateAttribute( '$id' => ID::custom($id), 'key' => $id, 'type' => $type, - 'size' => $size, + 'size' => $type === self::VAR_BIGINT ? (string)$size : $size, 'required' => $required, 'default' => $default, 'signed' => $signed, @@ -2523,6 +2561,7 @@ private function validateAttribute( filterCallback: fn ($id) => $this->adapter->filter($id), isMigrating: $this->isMigrating(), sharedTables: $this->getSharedTables(), + supportUnsignedBigInt: $this->adapter->getSupportForUnsignedBigInt(), ); $validator->isValid($attribute); @@ -2918,6 +2957,13 @@ public function updateAttribute(string $collection, string $id, ?string $type = } break; case self::VAR_BIGINT: + $sizeString = (string)$size; + $limit = ($this->adapter->getSupportForUnsignedBigInt() && !$signed) + ? BigIntHelper::UNSIGNED_MAX + : (string)$this->adapter->getLimitForBigInt(); + if (!BigIntHelper::fitsBigIntRange($sizeString, false, $this->adapter->getSupportForUnsignedBigInt() && !$signed)) { + throw new DatabaseException('Max size allowed for bigint is: ' . BigIntHelper::formatIntegerString($limit)); + } break; case self::VAR_FLOAT: case self::VAR_BOOLEAN: @@ -3024,7 +3070,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = ->setAttribute('$id', $newKey ?? $id) ->setattribute('key', $newKey ?? $id) ->setAttribute('type', $type) - ->setAttribute('size', $size) + ->setAttribute('size', $type === self::VAR_BIGINT ? (string)$size : $size) ->setAttribute('signed', $signed) ->setAttribute('array', $array) ->setAttribute('format', $format) @@ -5588,7 +5634,9 @@ public function createDocument(string $collection, Document $document): Document $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + null, + $this->adapter->getSupportForUnsignedBigInt() ); if (!$structure->isValid($document)) { throw new StructureException($structure->getDescription()); @@ -5696,7 +5744,9 @@ public function createDocuments( $this->adapter->getIdAttributeType(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + null, + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($document)) { throw new StructureException($validator->getDescription()); @@ -6389,7 +6439,8 @@ public function updateDocuments( $this->adapter->getMaxUIDLength(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($queries)) { @@ -6434,7 +6485,8 @@ public function updateDocuments( $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), $this->adapter->getSupportForAttributes(), - null // No old document available in bulk updates + null, // No old document available in bulk updates + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($updates)) { @@ -8097,7 +8149,8 @@ public function deleteDocuments( $this->adapter->getMaxUIDLength(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($queries)) { @@ -8319,7 +8372,8 @@ public function find(string $collection, array $queries = [], string $forPermiss $this->adapter->getMaxUIDLength(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); @@ -8575,7 +8629,8 @@ public function count(string $collection, array $queries = [], ?int $max = null) $this->adapter->getMaxUIDLength(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); @@ -8648,7 +8703,8 @@ public function sum(string $collection, string $attribute, array $queries = [], $this->adapter->getMaxUIDLength(), $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() + $this->adapter->getSupportForAttributes(), + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); @@ -8913,6 +8969,7 @@ public function casting(Document $collection, Document $document): Document foreach ($attributes as $attribute) { $key = $attribute['$id'] ?? ''; $type = $attribute['type'] ?? ''; + $signed = $attribute['signed'] ?? true; $array = $attribute['array'] ?? false; $value = $document->getAttribute($key, null); if (is_null($value)) { @@ -8946,7 +9003,9 @@ public function casting(Document $collection, Document $document): Document $node = (int)$node; break; case self::VAR_BIGINT: - $node = (int)$node; + if (\is_string($node) && BigIntHelper::fitsPhpInt($node, $signed)) { + $node = (int)$node; + } break; case self::VAR_FLOAT: $node = (float)$node; @@ -8964,7 +9023,6 @@ public function casting(Document $collection, Document $document): Document return $document; } - /** * Encode Attribute * diff --git a/src/Database/Helpers/BigInt.php b/src/Database/Helpers/BigInt.php new file mode 100644 index 000000000..06a9a0453 --- /dev/null +++ b/src/Database/Helpers/BigInt.php @@ -0,0 +1,93 @@ + $lenB) { + return 1; + } + if ($a === $b) { + return 0; + } + + return $a < $b ? -1 : 1; + } + + public static function formatIntegerString(string $value): string + { + $negative = \str_starts_with($value, '-'); + if ($negative) { + $value = \substr($value, 1); + } + + $value = self::normalizeUnsignedString($value); + $formatted = \preg_replace('/\B(?=(\d{3})+(?!\d))/', ',', $value) ?? $value; + + return $negative ? "-{$formatted}" : $formatted; + } +} diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index a924f189b..422312969 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -7,6 +7,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; +use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Validator; class Attribute extends Validator @@ -60,6 +61,7 @@ public function __construct( protected mixed $filterCallback = null, protected bool $isMigrating = false, protected bool $sharedTables = false, + protected bool $supportUnsignedBigInt = false, ) { // Keep backwards compatibility for existing validator construction sites. if ($this->maxBigIntLength === 0) { @@ -345,6 +347,21 @@ public function checkType(Document $attribute): bool break; case Database::VAR_BIGINT: + $sizeString = \is_string($size) ? $size : (string)$size; + if (!BigIntHelper::isIntegerString($sizeString, false)) { + $this->message = 'Size must be a positive integer'; + throw new DatabaseException($this->message); + } + + $sizeString = BigIntHelper::normalizeUnsignedString($sizeString); + $limit = (!$signed && $this->supportUnsignedBigInt) + ? BigIntHelper::UNSIGNED_MAX + : (string)$this->maxBigIntLength; + + if (BigIntHelper::compareUnsignedStrings($sizeString, $limit) > 0) { + $this->message = 'Max size allowed for bigint is: ' . BigIntHelper::formatIntegerString($limit); + throw new DatabaseException($this->message); + } break; case Database::VAR_FLOAT: diff --git a/src/Database/Validator/BigInt.php b/src/Database/Validator/BigInt.php new file mode 100644 index 000000000..2dd1332c1 --- /dev/null +++ b/src/Database/Validator/BigInt.php @@ -0,0 +1,51 @@ +signed) { + return 'Value must be a valid signed 64-bit integer between ' . + BigIntHelper::formatIntegerString(BigIntHelper::SIGNED_MIN) . + ' and ' . BigIntHelper::formatIntegerString(BigIntHelper::SIGNED_MAX); + } + + $max = $this->supportUnsigned64Bit ? BigIntHelper::UNSIGNED_MAX : BigIntHelper::SIGNED_MAX; + return 'Value must be a valid unsigned 64-bit integer between 0 and ' . + BigIntHelper::formatIntegerString($max); + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return self::TYPE_MIXED; + } + + public function isValid(mixed $value): bool + { + if (\is_int($value)) { + return $this->signed ? $value >= \PHP_INT_MIN && $value <= \PHP_INT_MAX : $value >= 0; + } + + if (!\is_string($value)) { + return false; + } + + return BigIntHelper::fitsBigIntRange($value, $this->signed, $this->supportUnsigned64Bit); + } +} diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php index fd8f5a989..a386a1fa4 100644 --- a/src/Database/Validator/PartialStructure.php +++ b/src/Database/Validator/PartialStructure.php @@ -53,7 +53,7 @@ public function isValid($document): bool return false; } - if (!$this->checkForInvalidAttributeValues($structure, $keys)) { + if (!$this->checkForInvalidAttributeValues($document, $structure, $keys)) { return false; } diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index e55852bb8..4959a062c 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -32,7 +32,8 @@ public function __construct( int $maxUIDLength = 36, \DateTime $minAllowedDate = new \DateTime('0000-01-01'), \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - bool $supportForAttributes = true + bool $supportForAttributes = true, + bool $supportUnsignedBigInt = true ) { $attributes[] = new Document([ '$id' => '$id', @@ -69,7 +70,8 @@ public function __construct( $maxValuesCount, $minAllowedDate, $maxAllowedDate, - $supportForAttributes + $supportForAttributes, + $supportUnsignedBigInt ), new Order($attributes, $supportForAttributes), new Select($attributes, $supportForAttributes), diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 83f40fba6..232a6a32c 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -5,6 +5,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; +use Utopia\Database\Validator\BigInt; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Sequence; use Utopia\Validator\Boolean; @@ -31,7 +32,8 @@ public function __construct( private readonly int $maxValuesCount = 5000, private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), - private bool $supportForAttributes = true + private bool $supportForAttributes = true, + private readonly bool $supportUnsignedBigInt = true ) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getId())] = $attribute->getArrayCopy(); @@ -153,7 +155,6 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s break; case Database::VAR_INTEGER: - case Database::VAR_BIGINT: $size = $attributeSchema['size'] ?? 4; $signed = $attributeSchema['signed'] ?? true; $bits = ($attributeType === Database::VAR_BIGINT || $size >= 8) ? 64 : 32; @@ -162,6 +163,11 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $validator = new Integer(false, $bits, $unsigned); break; + case Database::VAR_BIGINT: + $signed = $attributeSchema['signed'] ?? true; + $validator = new BigInt($signed, $this->supportUnsignedBigInt); + break; + case Database::VAR_FLOAT: $validator = new FloatValidator(); break; diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 09c4efd29..1609675f9 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -7,6 +7,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Database\Operator; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Operator as OperatorValidator; @@ -109,7 +110,8 @@ public function __construct( private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), private bool $supportForAttributes = true, - private readonly ?Document $currentDocument = null + private readonly ?Document $currentDocument = null, + private readonly bool $supportUnsignedBigInt = true ) { } @@ -237,7 +239,7 @@ public function isValid($document): bool return false; } - if (!$this->checkForInvalidAttributeValues($structure, $keys)) { + if (!$this->checkForInvalidAttributeValues($document, $structure, $keys)) { return false; } @@ -305,7 +307,7 @@ protected function checkForUnknownAttributes(array $structure, array $keys): boo * * @return bool */ - protected function checkForInvalidAttributeValues(array $structure, array $keys): bool + protected function checkForInvalidAttributeValues(Document $document, array $structure, array $keys): bool { foreach ($structure as $key => $value) { if (Operator::isOperator($value)) { @@ -336,6 +338,15 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) continue; } + // BIGINT accepts both PHP int and numeric strings. + // If the numeric string is within PHP's int range, normalize it to an int + // so downstream code gets a numeric value without precision loss. + if ($type === Database::VAR_BIGINT && \is_string($value) && $this->isBigIntStringWithinPhpIntRange($value, $signed)) { + $normalized = (int)$value; + $document->setAttribute($key, $normalized); + $value = $normalized; + } + $validators = []; switch ($type) { @@ -352,7 +363,6 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) break; case Database::VAR_INTEGER: - case Database::VAR_BIGINT: // Determine bit size based on attribute size in bytes // BIGINT is always 64-bit in SQL adapters; VAR_INTEGER uses size to decide. $bits = ($type === Database::VAR_BIGINT || $size >= 8) ? 64 : 32; @@ -365,6 +375,10 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) $validators[] = new Range($min, $max, Database::VAR_INTEGER); break; + case Database::VAR_BIGINT: + $validators[] = new BigInt($signed, $this->supportUnsignedBigInt); + break; + case Database::VAR_FLOAT: // We need both Float and Range because Range implicitly casts non-numeric values $validators[] = new FloatValidator(); @@ -448,6 +462,11 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) return true; } + public function isBigIntStringWithinPhpIntRange(string $value, bool $signed): bool + { + return BigIntHelper::fitsPhpInt($value, $signed); + } + /** * Is array * diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 3c5633734..c61f7b662 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2222,7 +2222,7 @@ public function testCreateAttributesIntegerSizeLimit(): void } - public function testCreateAttributesBigInt(): void + public function testCreateAttributesBigIntIgnoresSizeLimit(): void { /** @var Database $database */ $database = $this->getDatabase(); @@ -2232,7 +2232,8 @@ public function testCreateAttributesBigInt(): void return; } - $database->createCollection(__FUNCTION__); + $collectionName = 'bigint_ignores_size_limit'; + $database->createCollection($collectionName); $limit = $database->getAdapter()->getLimitForBigInt() / 2; $size = (int)$limit + 1; @@ -2244,14 +2245,87 @@ public function testCreateAttributesBigInt(): void 'required' => false ]]; - $result = $database->createAttributes(__FUNCTION__, $attributes); + $result = $database->createAttributes($collectionName, $attributes); $this->assertTrue($result); - $collection = $database->getCollection(__FUNCTION__); + $collection = $database->getCollection($collectionName); $attrs = $collection->getAttribute('attributes'); $this->assertCount(1, $attrs); $this->assertEquals('foo', $attrs[0]['$id']); - $this->assertEquals($size, $attrs[0]['size']); + $this->assertEquals((string)$size, (string)$attrs[0]['size']); + } + + public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadata(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $collectionName = 'bigint_attr_validation'; + $database->createCollection($collectionName); + + $this->assertTrue($database->createAttribute( + $collectionName, + 'signed_bigint', + Database::VAR_BIGINT, + 0, + false, + signed: true + )); + $this->assertTrue($database->createAttribute( + $collectionName, + 'unsigned_bigint', + Database::VAR_BIGINT, + 0, + false, + signed: false + )); + + $collection = $database->getCollection($collectionName); + $attributes = $collection->getAttribute('attributes', []); + + $signedAttribute = null; + $unsignedAttribute = null; + foreach ($attributes as $attribute) { + if (($attribute['$id'] ?? '') === 'signed_bigint') { + $signedAttribute = $attribute; + } + if (($attribute['$id'] ?? '') === 'unsigned_bigint') { + $unsignedAttribute = $attribute; + } + } + + $this->assertNotNull($signedAttribute); + $this->assertNotNull($unsignedAttribute); + $this->assertTrue($signedAttribute['signed']); + $this->assertFalse($unsignedAttribute['signed']); + $this->assertEquals('0', (string)$signedAttribute['size']); + $this->assertEquals('0', (string)$unsignedAttribute['size']); + + // Signed overflow should always fail. + try { + $database->updateAttribute($collectionName, 'signed_bigint', size: Database::MAX_BIG_INT + 1); + $this->fail('Expected DatabaseException for signed bigint size overflow'); + } catch (\Throwable $e) { + $this->assertInstanceOf(DatabaseException::class, $e); + $this->assertStringContainsString('Max size allowed for bigint', $e->getMessage()); + } + + // Unsigned overflow behavior depends on adapter support. + if ($database->getAdapter()->getSupportForUnsignedBigInt()) { + // Should accept values above signed max for unsigned bigint. + $this->assertInstanceOf( + Document::class, + $database->updateAttribute($collectionName, 'unsigned_bigint', size: Database::MAX_BIG_INT + 1) + ); + } else { + try { + $database->updateAttribute($collectionName, 'unsigned_bigint', size: Database::MAX_BIG_INT + 1); + $this->fail('Expected DatabaseException for unsigned bigint overflow on adapter without unsigned bigint support'); + } catch (\Throwable $e) { + $this->assertInstanceOf(DatabaseException::class, $e); + $this->assertStringContainsString('Max size allowed for bigint', $e->getMessage()); + } + } } public function testCreateAttributesSuccessMultiple(): void diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 673f57409..21e3de96e 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -131,6 +131,134 @@ public function testCreateDocumentWithBigIntType(): void $this->assertEquals('bigint-type-doc', $results[0]->getId()); } + public function testBigIntScenariosWithFiltering(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + if (!$database->getAdapter()->getSupportForUnsignedBigInt()) { + $this->markTestSkipped('Adapter does not support unsigned bigint'); + } + + $collection = 'bigint_scenarios_filters'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'signed_bigint', Database::VAR_BIGINT, 0, true)); + $this->assertEquals(true, $database->createAttribute($collection, 'unsigned_bigint', Database::VAR_BIGINT, 0, true, signed: false)); + + $collectionDoc = $database->getCollection($collection); + $this->assertEquals($collection, $collectionDoc->getId()); + $attributes = $collectionDoc->getAttribute('attributes', []); + $signedAttr = null; + $unsignedAttr = null; + foreach ($attributes as $attribute) { + if (($attribute->getAttribute('$id') ?? '') === 'signed_bigint') { + $signedAttr = $attribute; + } + if (($attribute->getAttribute('$id') ?? '') === 'unsigned_bigint') { + $unsignedAttr = $attribute; + } + } + + $this->assertNotNull($signedAttr); + $this->assertNotNull($unsignedAttr); + $this->assertSame('0', $signedAttr->getAttribute('size')); + $this->assertSame('0', $unsignedAttr->getAttribute('size')); + + // "Out of regular int limit" (32-bit) but valid bigint should still normalize to PHP int. + $beyond32Bit = '2147483648'; + $signedMax = (string)\PHP_INT_MAX; + $signedMin = (string)\PHP_INT_MIN; + $unsignedValue = '18446744073709551615'; + + $document = $database->createDocument($collection, new Document([ + '$id' => 'bigint-scenarios-doc', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + 'signed_bigint' => $beyond32Bit, + 'unsigned_bigint' => $unsignedValue, + ])); + + $this->assertIsInt($document->getAttribute('signed_bigint')); + $this->assertEquals((int)$beyond32Bit, $document->getAttribute('signed_bigint')); + + // Compare by string representation to stay adapter-agnostic (int/string return type differs). + $this->assertEquals($unsignedValue, (string)$document->getAttribute('unsigned_bigint')); + $this->assertTrue(\is_string($document->getAttribute('unsigned_bigint'))); + + // Update path should apply the same normalization for signed bigint numeric strings. + $updated = $database->updateDocument($collection, $document->getId(), new Document([ + 'signed_bigint' => $signedMax, + ])); + $this->assertIsInt($updated->getAttribute('signed_bigint')); + $this->assertEquals((int)$signedMax, $updated->getAttribute('signed_bigint')); + + // Filtering tests: both int and numeric-string filters should match bigint fields. + $resultIntFilter = $database->find($collection, [ + Query::equal('signed_bigint', [(int)$signedMax]), + ]); + $this->assertCount(1, $resultIntFilter); + $this->assertEquals('bigint-scenarios-doc', $resultIntFilter[0]->getId()); + + $resultStringFilter = $database->find($collection, [ + Query::equal('signed_bigint', [$signedMax]), + ]); + $this->assertCount(1, $resultStringFilter); + $this->assertEquals('bigint-scenarios-doc', $resultStringFilter[0]->getId()); + + $resultUnsignedFilter = $database->find($collection, [ + Query::equal('unsigned_bigint', [$unsignedValue]), + ]); + $this->assertCount(1, $resultUnsignedFilter); + $this->assertEquals('bigint-scenarios-doc', $resultUnsignedFilter[0]->getId()); + + // Lower signed boundary as numeric-string should also normalize to int. + $updatedMin = $database->updateDocument($collection, $document->getId(), new Document([ + 'signed_bigint' => $signedMin, + ])); + $this->assertIsInt($updatedMin->getAttribute('signed_bigint')); + $this->assertEquals((int)$signedMin, $updatedMin->getAttribute('signed_bigint')); + } + + public function testWithSingedBigInt(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $collection = 'signed_bigint_only'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'signed_bigint', Database::VAR_BIGINT, 0, true)); + + $signedMin = (string)\PHP_INT_MIN; + $signedMax = (string)\PHP_INT_MAX; + + $document = $database->createDocument($collection, new Document([ + '$id' => 'signed-bigint-doc', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + 'signed_bigint' => $signedMax, + ])); + + $this->assertIsInt($document->getAttribute('signed_bigint')); + $this->assertEquals((int)$signedMax, $document->getAttribute('signed_bigint')); + + $updated = $database->updateDocument($collection, $document->getId(), new Document([ + 'signed_bigint' => $signedMin, + ])); + + $this->assertIsInt($updated->getAttribute('signed_bigint')); + $this->assertEquals((int)$signedMin, $updated->getAttribute('signed_bigint')); + + $results = $database->find($collection, [ + Query::equal('signed_bigint', [$signedMin]), + ]); + $this->assertCount(1, $results); + $this->assertEquals('signed-bigint-doc', $results[0]->getId()); + } + public function testCreateDocument(): Document { /** @var Database $database */ @@ -1522,7 +1650,7 @@ public function testCreateUpdateBigIntAndIncrementDecrement(): void // Verify regular update works for bigint attributes $updated = $database->updateDocument($collection, $document->getId(), new Document([ - 'inc' => 20 + 'inc' => 20, ])); $this->assertEquals(20, $updated->getAttribute('inc')); @@ -7789,4 +7917,28 @@ public function testRegexInjection(): void // } // $database->deleteCollection($collectionName); // } + + public function testCreateDocumentWithBigIntStringValues(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $collection = 'test_big_int_string_values'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'bigint_unsigned', Database::VAR_BIGINT, 0, false, signed: false)); + + $database->createDocument($collection, new Document([ + '$id' => 'bigint-string-type-doc', + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], + 'bigint_unsigned' => (string)9223372036854775807, + ])); + + $data = $database->getDocument($collection, 'bigint-string-type-doc'); + $this->assertFalse($data->isEmpty()); + $this->assertEquals('bigint-string-type-doc', $data->getId()); + $this->assertEquals('9223372036854775807', (string)$data->getAttribute('bigint_unsigned')); + } } diff --git a/tests/unit/Validator/Query/FilterTest.php b/tests/unit/Validator/Query/FilterTest.php index a0ec65eeb..21ba8f404 100644 --- a/tests/unit/Validator/Query/FilterTest.php +++ b/tests/unit/Validator/Query/FilterTest.php @@ -42,6 +42,20 @@ public function setUp(): void 'type' => Database::VAR_INTEGER, 'array' => false, ]), + new Document([ + '$id' => 'bigint_unsigned', + 'key' => 'bigint_unsigned', + 'type' => Database::VAR_BIGINT, + 'array' => false, + 'signed' => false, + ]), + new Document([ + '$id' => 'bigint_signed', + 'key' => 'bigint_signed', + 'type' => Database::VAR_BIGINT, + 'array' => false, + 'signed' => true, + ]), ]; $this->validator = new Filter( @@ -61,6 +75,8 @@ public function testSuccess(): void $this->assertTrue($this->validator->isValid(Query::contains('integer_array', [100,10,-1]))); $this->assertTrue($this->validator->isValid(Query::contains('string_array', ["1","10","-1"]))); $this->assertTrue($this->validator->isValid(Query::contains('string', ['super']))); + $this->assertTrue($this->validator->isValid(Query::equal('bigint_unsigned', ['18446744073709551615']))); + $this->assertTrue($this->validator->isValid(Query::equal('bigint_signed', ['-9223372036854775808']))); } public function testFailure(): void diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index f3b49864d..10afeaa0a 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -1135,4 +1135,122 @@ public function testStringTypeArrayValidation(): void $this->assertEquals('Invalid document structure: Attribute "varchar_array[\'0\']" has invalid type. Value must be a valid string and no longer than 128 chars', $validator->getDescription()); } + public function testBigIntSignedAcceptsNumericStringAndNormalizesToInt(): void + { + $collection = [ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'collections', + 'attributes' => [ + [ + '$id' => 'bigint_signed', + 'type' => Database::VAR_BIGINT, + 'format' => '', + 'size' => 0, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]; + + $validator = new Structure( + new Document($collection), + Database::VAR_INTEGER + ); + + $doc = new Document([ + '$collection' => ID::custom('posts'), + 'bigint_signed' => (string)PHP_INT_MAX, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]); + + $this->assertTrue($validator->isValid($doc)); + $this->assertIsInt($doc->getAttribute('bigint_signed')); + $this->assertEquals(PHP_INT_MAX, $doc->getAttribute('bigint_signed')); + } + + public function testBigIntUnsignedAcceptsLargeNumericStringAsString(): void + { + $collection = [ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'collections', + 'attributes' => [ + [ + '$id' => 'bigint_unsigned', + 'type' => Database::VAR_BIGINT, + 'format' => '', + 'size' => 0, + 'required' => true, + 'signed' => false, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]; + + $validator = new Structure( + new Document($collection), + Database::VAR_INTEGER + ); + + $unsignedMax = '18446744073709551615'; + + $doc = new Document([ + '$collection' => ID::custom('posts'), + 'bigint_unsigned' => $unsignedMax, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]); + + $this->assertTrue($validator->isValid($doc)); + $this->assertIsString($doc->getAttribute('bigint_unsigned')); + $this->assertEquals($unsignedMax, $doc->getAttribute('bigint_unsigned')); + } + + public function testBigIntUnsignedRejectsNegativeNumericString(): void + { + $collection = [ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'collections', + 'attributes' => [ + [ + '$id' => 'bigint_unsigned', + 'type' => Database::VAR_BIGINT, + 'format' => '', + 'size' => 0, + 'required' => true, + 'signed' => false, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]; + + $validator = new Structure( + new Document($collection), + Database::VAR_INTEGER + ); + + $doc = new Document([ + '$collection' => ID::custom('posts'), + 'bigint_unsigned' => '-1', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]); + + $this->assertFalse($validator->isValid($doc)); + $this->assertEquals( + 'Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid unsigned 64-bit integer between 0 and 18,446,744,073,709,551,615', + $validator->getDescription() + ); + } + } From 38e31f4fdfcf749d79e318c5b040cffc4834b8f4 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 16:51:51 +0530 Subject: [PATCH 19/29] updated --- src/Database/Database.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 0fd9a3333..8c90d78f0 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6318,7 +6318,8 @@ public function updateDocument(string $collection, string $id, Document $documen $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), $this->adapter->getSupportForAttributes(), - $old + $old, + $this->adapter->getSupportForUnsignedBigInt() ); if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($structureValidator->getDescription()); @@ -7332,7 +7333,8 @@ public function upsertDocumentsWithIncrease( $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), $this->adapter->getSupportForAttributes(), - $old->isEmpty() ? null : $old + $old->isEmpty() ? null : $old, + $this->adapter->getSupportForUnsignedBigInt() ); if (!$validator->isValid($document)) { From cafb3532b55b4f9c9340999c0870e90358c42b98 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 16:53:43 +0530 Subject: [PATCH 20/29] updated --- tests/e2e/Adapter/Scopes/AttributeTests.php | 35 ++++++++++++--------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index c61f7b662..825c38ada 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2301,25 +2301,32 @@ public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadat $this->assertEquals('0', (string)$signedAttribute['size']); $this->assertEquals('0', (string)$unsignedAttribute['size']); - // Signed overflow should always fail. - try { - $database->updateAttribute($collectionName, 'signed_bigint', size: Database::MAX_BIG_INT + 1); - $this->fail('Expected DatabaseException for signed bigint size overflow'); - } catch (\Throwable $e) { - $this->assertInstanceOf(DatabaseException::class, $e); - $this->assertStringContainsString('Max size allowed for bigint', $e->getMessage()); + // Signed overflow check (only when we can build an int beyond adapter signed max). + $signedLimit = (int)$database->getAdapter()->getLimitForBigInt(); + if ($signedLimit < \PHP_INT_MAX) { + try { + $database->updateAttribute($collectionName, 'signed_bigint', size: $signedLimit + 1); + $this->fail('Expected DatabaseException for signed bigint size overflow'); + } catch (\Throwable $e) { + $this->assertInstanceOf(DatabaseException::class, $e); + $this->assertStringContainsString('Max size allowed for bigint', $e->getMessage()); + } } - // Unsigned overflow behavior depends on adapter support. + // Unsigned > signed-max behavior validated through batch create (string size avoids int overflow). + $largeUnsignedAttribute = [[ + '$id' => 'unsigned_bigint_large', + 'type' => Database::VAR_BIGINT, + 'size' => '18446744073709551615', + 'required' => false, + 'signed' => false, + ]]; + if ($database->getAdapter()->getSupportForUnsignedBigInt()) { - // Should accept values above signed max for unsigned bigint. - $this->assertInstanceOf( - Document::class, - $database->updateAttribute($collectionName, 'unsigned_bigint', size: Database::MAX_BIG_INT + 1) - ); + $this->assertTrue($database->createAttributes($collectionName, $largeUnsignedAttribute)); } else { try { - $database->updateAttribute($collectionName, 'unsigned_bigint', size: Database::MAX_BIG_INT + 1); + $database->createAttributes($collectionName, $largeUnsignedAttribute); $this->fail('Expected DatabaseException for unsigned bigint overflow on adapter without unsigned bigint support'); } catch (\Throwable $e) { $this->assertInstanceOf(DatabaseException::class, $e); From dbe3537a04f425d4732444dbb77cff2c91025d4b Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 18:14:08 +0530 Subject: [PATCH 21/29] Refactor attribute size handling in database adapters to support both int and string types - Updated `createAttribute`, `updateAttribute`, and `getColumnType` methods across various database adapters to accept `int|string` for the size parameter. - Introduced a new private method `getSize` in the `Database` class to normalize size handling based on attribute type. - Enhanced tests to ensure proper functionality with the updated size handling. --- src/Database/Adapter.php | 6 ++--- src/Database/Adapter/MariaDB.php | 4 ++-- src/Database/Adapter/Mongo.php | 4 ++-- src/Database/Adapter/Pool.php | 4 ++-- src/Database/Adapter/Postgres.php | 6 ++--- src/Database/Adapter/SQL.php | 6 ++--- src/Database/Adapter/SQLite.php | 2 +- src/Database/Database.php | 28 +++++++++++++++------- src/Database/Mirror.php | 4 ++-- tests/e2e/Adapter/Scopes/DocumentTests.php | 8 +++++-- 10 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 6e5dec54b..6e80fcb04 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -576,7 +576,7 @@ abstract public function analyzeCollection(string $collection): bool; * @throws TimeoutException * @throws DuplicateException */ - abstract public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool; + abstract public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool; /** * Create Attributes @@ -603,7 +603,7 @@ abstract public function createAttributes(string $collection, array $attributes) * * @return bool */ - abstract public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool; + abstract public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool; /** * Delete Attribute @@ -1405,7 +1405,7 @@ abstract public function getSchemaIndexes(string $collection): array; * @return string * @throws \Utopia\Database\Exception For unknown types on adapters that support column-type resolution. */ - public function getColumnType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string + public function getColumnType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string { return ''; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e45e9f6ac..ded2f3573 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -418,7 +418,7 @@ public function getSchemaAttributes(string $collection): array * @return bool * @throws DatabaseException */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { $name = $this->filter($collection); $id = $this->filter($id); @@ -1678,7 +1678,7 @@ protected function getSQLCondition(Query $query, array &$binds): string * @return string * @throws DatabaseException */ - protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string + protected function getSQLType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string { if (in_array($type, Database::SPATIAL_TYPES)) { return $this->getSpatialSQLType($type, $required); diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 2f90e11d5..89b417afd 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -702,7 +702,7 @@ public function analyzeCollection(string $collection): bool * @param bool $array * @return bool */ - public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool { return true; } @@ -1983,7 +1983,7 @@ public function deleteDocuments(string $collection, array $sequences, array $per * * @return bool */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { if (!empty($newKey) && $newKey !== $id) { return $this->renameAttribute($collection, $id, $newKey); diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index ddf90c09b..4ee34204a 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -203,7 +203,7 @@ public function analyzeCollection(string $collection): bool return $this->delegate(__FUNCTION__, \func_get_args()); } - public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } @@ -213,7 +213,7 @@ public function createAttributes(string $collection, array $attributes): bool return $this->delegate(__FUNCTION__, \func_get_args()); } - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 7362dc265..91787d907 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -470,7 +470,7 @@ public function analyzeCollection(string $collection): bool * @return bool * @throws DatabaseException */ - public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool { // Ensure pgvector extension is installed for vector types if ($type === Database::VAR_VECTOR) { @@ -577,7 +577,7 @@ public function renameAttribute(string $collection, string $old, string $new): b * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { $name = $this->filter($collection); $id = $this->filter($id); @@ -1938,7 +1938,7 @@ protected function getFulltextValue(string $value): string * @return string * @throws DatabaseException */ - protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string + protected function getSQLType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string { if ($array === true) { return 'JSONB'; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index baca6b4e7..77951bd86 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -243,7 +243,7 @@ public function list(): array * @throws Exception * @throws PDOException */ - public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool { $id = $this->quote($this->filter($id)); $type = $this->getSQLType($type, $size, $signed, $array, $required); @@ -1837,7 +1837,7 @@ protected function getSQLOperator(string $method): string abstract protected function getSQLType( string $type, - int $size, + int|string $size, bool $signed = true, bool $array = false, bool $required = false @@ -1846,7 +1846,7 @@ abstract protected function getSQLType( /** * @throws DatabaseException For unknown type values. */ - public function getColumnType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string + public function getColumnType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string { return $this->getSQLType($type, $size, $signed, $array, $required); } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 59f173278..ad10dd650 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -336,7 +336,7 @@ public function analyzeCollection(string $collection): bool * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { if (!empty($newKey) && $newKey !== $id) { return $this->renameAttribute($collection, $id, $newKey); diff --git a/src/Database/Database.php b/src/Database/Database.php index 8c90d78f0..433c0caf8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1987,6 +1987,15 @@ private function normalizeCollectionAttributeSizes(Document $collection): Docume return $collection; } + private function getSize(string $type, int|string|null $size): int|string + { + if ($type === self::VAR_BIGINT) { + return (string)($size ?? 0); + } + + return (int)($size ?? 0); + } + /** * Get Collection Size * @@ -2146,8 +2155,9 @@ public function deleteCollection(string $id): bool * @throws StructureException * @throws Exception */ - public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, mixed $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool + public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $required, mixed $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool { + $size = $this->getSize($type, $size); $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { @@ -2231,7 +2241,7 @@ public function createAttribute(string $collection, string $id, string $type, in '$id' => ID::custom($id), 'key' => $id, 'type' => $type, - 'size' => $type === self::VAR_BIGINT ? (string)$size : $size, + 'size' => $this->getSize($type, $size), 'required' => $required, 'default' => $default, 'signed' => $signed, @@ -2355,6 +2365,7 @@ public function createAttributes(string $collection, array $attributes): bool $existsInSchema = false; try { + $attribute['size'] = $this->getSize($attribute['type'], $attribute['size']); $attributeDocument = $this->validateAttribute( $collection, $attribute['$id'], @@ -2411,7 +2422,7 @@ public function createAttributes(string $collection, array $attributes): bool '$id' => ID::custom($attribute['$id']), 'key' => $attribute['$id'], 'type' => $attribute['type'], - 'size' => $attribute['size'], + 'size' => $this->getSize($attribute['type'], $attribute['size']), 'required' => $attribute['required'], 'default' => $attribute['default'], 'signed' => $attribute['signed'], @@ -2514,7 +2525,7 @@ private function validateAttribute( Document $collection, string $id, string $type, - int $size, + int|string $size, bool $required, mixed $default, bool $signed, @@ -2528,7 +2539,7 @@ private function validateAttribute( '$id' => ID::custom($id), 'key' => $id, 'type' => $type, - 'size' => $type === self::VAR_BIGINT ? (string)$size : $size, + 'size' => $this->getSize($type, $size), 'required' => $required, 'default' => $default, 'signed' => $signed, @@ -2870,7 +2881,7 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de * @return Document * @throws Exception */ - public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, int|string|null $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { $collectionDoc = $this->silent(fn () => $this->getCollection($collection)); @@ -2906,6 +2917,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = || !\is_null($newKey); $type ??= $attribute->getAttribute('type'); $size ??= $attribute->getAttribute('size'); + $size = $this->getSize($type, $size); $signed ??= $attribute->getAttribute('signed'); $required ??= $attribute->getAttribute('required'); $default ??= $attribute->getAttribute('default'); @@ -3070,7 +3082,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = ->setAttribute('$id', $newKey ?? $id) ->setattribute('key', $newKey ?? $id) ->setAttribute('type', $type) - ->setAttribute('size', $type === self::VAR_BIGINT ? (string)$size : $size) + ->setAttribute('size', $this->getSize($type, $size)) ->setAttribute('signed', $signed) ->setAttribute('array', $array) ->setAttribute('format', $format) @@ -3194,7 +3206,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = $collection, $newKey ?? $id, $originalType, - $originalSize, + (int)$originalSize, $originalSigned, $originalArray, $originalKey, diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index f740cab3e..6cf55d35e 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -301,7 +301,7 @@ public function deleteCollection(string $id): bool return $result; } - public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool + public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $required, $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool { $result = $this->source->createAttribute( $collection, @@ -399,7 +399,7 @@ public function createAttributes(string $collection, array $attributes): bool return $result; } - public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, int|string|null $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { $document = $this->source->updateAttribute( $collection, diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 21e3de96e..87026812e 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -187,6 +187,10 @@ public function testBigIntScenariosWithFiltering(): void $this->assertEquals($unsignedValue, (string)$document->getAttribute('unsigned_bigint')); $this->assertTrue(\is_string($document->getAttribute('unsigned_bigint'))); + // Read path: fetch document and ensure unsigned bigint round-trips unchanged. + $fetchedDocument = $database->getDocument($collection, $document->getId()); + $this->assertEquals($unsignedValue, (string)$fetchedDocument->getAttribute('unsigned_bigint')); + // Update path should apply the same normalization for signed bigint numeric strings. $updated = $database->updateDocument($collection, $document->getId(), new Document([ 'signed_bigint' => $signedMax, @@ -230,8 +234,8 @@ public function testWithSingedBigInt(): void $database->createCollection($collection); $this->assertEquals(true, $database->createAttribute($collection, 'signed_bigint', Database::VAR_BIGINT, 0, true)); - $signedMin = (string)\PHP_INT_MIN; - $signedMax = (string)\PHP_INT_MAX; + $signedMin = \PHP_INT_MIN; + $signedMax = \PHP_INT_MAX; $document = $database->createDocument($collection, new Document([ '$id' => 'signed-bigint-doc', From ba45e22f661db542a40437e284f1d2e4b218eaa0 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 18:16:35 +0530 Subject: [PATCH 22/29] Enhance attribute validation to support string type for big integers - Updated the validation logic in the `Attribute` class to allow for string types as valid default values for `bigint` attributes, improving type flexibility and error handling. --- src/Database/Validator/Attribute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 422312969..5d1b71639 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -552,7 +552,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void } break; case Database::VAR_BIGINT: - if ($defaultType !== 'integer') { + if ($defaultType !== 'integer' && $defaultType !== 'string') { $this->message = 'Default value ' . $default . ' does not match given type ' . $type; throw new DatabaseException($this->message); } From 75a7f28135956118b59e1482efc2f816dd4ce3a8 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 19:30:38 +0530 Subject: [PATCH 23/29] Refactor attribute size handling to enforce integer type across database adapters - Updated `createAttribute`, `updateAttribute`, and `getColumnType` methods in various database adapters to require `int` for the size parameter, removing support for `string`. - Removed the `getSize` method from the `Database` class as it is no longer needed. - Adjusted related tests to validate the new integer-only size handling. --- src/Database/Adapter.php | 6 +-- src/Database/Adapter/MariaDB.php | 4 +- src/Database/Adapter/Mongo.php | 4 +- src/Database/Adapter/Pool.php | 4 +- src/Database/Adapter/Postgres.php | 6 +-- src/Database/Adapter/SQL.php | 6 +-- src/Database/Adapter/SQLite.php | 2 +- src/Database/Database.php | 52 +++++++++------------ src/Database/Mirror.php | 4 +- src/Database/Validator/Attribute.php | 16 ------- tests/e2e/Adapter/Scopes/AttributeTests.php | 6 +-- tests/e2e/Adapter/Scopes/DocumentTests.php | 4 +- 12 files changed, 45 insertions(+), 69 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 6e80fcb04..6e5dec54b 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -576,7 +576,7 @@ abstract public function analyzeCollection(string $collection): bool; * @throws TimeoutException * @throws DuplicateException */ - abstract public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool; + abstract public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool; /** * Create Attributes @@ -603,7 +603,7 @@ abstract public function createAttributes(string $collection, array $attributes) * * @return bool */ - abstract public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool; + abstract public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool; /** * Delete Attribute @@ -1405,7 +1405,7 @@ abstract public function getSchemaIndexes(string $collection): array; * @return string * @throws \Utopia\Database\Exception For unknown types on adapters that support column-type resolution. */ - public function getColumnType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string + public function getColumnType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string { return ''; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ded2f3573..e45e9f6ac 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -418,7 +418,7 @@ public function getSchemaAttributes(string $collection): array * @return bool * @throws DatabaseException */ - public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { $name = $this->filter($collection); $id = $this->filter($id); @@ -1678,7 +1678,7 @@ protected function getSQLCondition(Query $query, array &$binds): string * @return string * @throws DatabaseException */ - protected function getSQLType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string + protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string { if (in_array($type, Database::SPATIAL_TYPES)) { return $this->getSpatialSQLType($type, $required); diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 89b417afd..2f90e11d5 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -702,7 +702,7 @@ public function analyzeCollection(string $collection): bool * @param bool $array * @return bool */ - public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool { return true; } @@ -1983,7 +1983,7 @@ public function deleteDocuments(string $collection, array $sequences, array $per * * @return bool */ - public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { if (!empty($newKey) && $newKey !== $id) { return $this->renameAttribute($collection, $id, $newKey); diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index 4ee34204a..ddf90c09b 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -203,7 +203,7 @@ public function analyzeCollection(string $collection): bool return $this->delegate(__FUNCTION__, \func_get_args()); } - public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } @@ -213,7 +213,7 @@ public function createAttributes(string $collection, array $attributes): bool return $this->delegate(__FUNCTION__, \func_get_args()); } - public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { return $this->delegate(__FUNCTION__, \func_get_args()); } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 91787d907..7362dc265 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -470,7 +470,7 @@ public function analyzeCollection(string $collection): bool * @return bool * @throws DatabaseException */ - public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool { // Ensure pgvector extension is installed for vector types if ($type === Database::VAR_VECTOR) { @@ -577,7 +577,7 @@ public function renameAttribute(string $collection, string $old, string $new): b * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { $name = $this->filter($collection); $id = $this->filter($id); @@ -1938,7 +1938,7 @@ protected function getFulltextValue(string $value): string * @return string * @throws DatabaseException */ - protected function getSQLType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string + protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string { if ($array === true) { return 'JSONB'; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 77951bd86..baca6b4e7 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -243,7 +243,7 @@ public function list(): array * @throws Exception * @throws PDOException */ - public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): bool { $id = $this->quote($this->filter($id)); $type = $this->getSQLType($type, $size, $signed, $array, $required); @@ -1837,7 +1837,7 @@ protected function getSQLOperator(string $method): string abstract protected function getSQLType( string $type, - int|string $size, + int $size, bool $signed = true, bool $array = false, bool $required = false @@ -1846,7 +1846,7 @@ abstract protected function getSQLType( /** * @throws DatabaseException For unknown type values. */ - public function getColumnType(string $type, int|string $size, bool $signed = true, bool $array = false, bool $required = false): string + public function getColumnType(string $type, int $size, bool $signed = true, bool $array = false, bool $required = false): string { return $this->getSQLType($type, $size, $signed, $array, $required); } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index ad10dd650..59f173278 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -336,7 +336,7 @@ public function analyzeCollection(string $collection): bool * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int|string $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null, bool $required = false): bool { if (!empty($newKey) && $newKey !== $id) { return $this->renameAttribute($collection, $id, $newKey); diff --git a/src/Database/Database.php b/src/Database/Database.php index 433c0caf8..604b63840 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1970,16 +1970,19 @@ private function normalizeCollectionAttributeSizes(Document $collection): Docume } $type = $attribute->getAttribute('type'); - $size = $attribute->getAttribute('size', 0); if ($type === self::VAR_BIGINT) { - // Keep bigint size as string to avoid precision loss. - $attribute->setAttribute('size', (string)$size); - continue; + $formatOptions = $attribute->getAttribute('formatOptions', []); + if (\is_array($formatOptions)) { + if (\array_key_exists('min', $formatOptions) && $formatOptions['min'] !== null) { + $formatOptions['min'] = (string)$formatOptions['min']; + } + if (\array_key_exists('max', $formatOptions) && $formatOptions['max'] !== null) { + $formatOptions['max'] = (string)$formatOptions['max']; + } + $attribute->setAttribute('formatOptions', $formatOptions); + } } - - // Keep legacy behavior for non-bigint metadata. - $attribute->setAttribute('size', (int)$size); } $collection->setAttribute('attributes', $attributes); @@ -1987,15 +1990,6 @@ private function normalizeCollectionAttributeSizes(Document $collection): Docume return $collection; } - private function getSize(string $type, int|string|null $size): int|string - { - if ($type === self::VAR_BIGINT) { - return (string)($size ?? 0); - } - - return (int)($size ?? 0); - } - /** * Get Collection Size * @@ -2155,9 +2149,8 @@ public function deleteCollection(string $id): bool * @throws StructureException * @throws Exception */ - public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $required, mixed $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, mixed $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool { - $size = $this->getSize($type, $size); $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { @@ -2241,7 +2234,7 @@ public function createAttribute(string $collection, string $id, string $type, in '$id' => ID::custom($id), 'key' => $id, 'type' => $type, - 'size' => $this->getSize($type, $size), + 'size' => $size, 'required' => $required, 'default' => $default, 'signed' => $signed, @@ -2365,7 +2358,6 @@ public function createAttributes(string $collection, array $attributes): bool $existsInSchema = false; try { - $attribute['size'] = $this->getSize($attribute['type'], $attribute['size']); $attributeDocument = $this->validateAttribute( $collection, $attribute['$id'], @@ -2422,7 +2414,7 @@ public function createAttributes(string $collection, array $attributes): bool '$id' => ID::custom($attribute['$id']), 'key' => $attribute['$id'], 'type' => $attribute['type'], - 'size' => $this->getSize($attribute['type'], $attribute['size']), + 'size' => $attribute['size'], 'required' => $attribute['required'], 'default' => $attribute['default'], 'signed' => $attribute['signed'], @@ -2525,7 +2517,7 @@ private function validateAttribute( Document $collection, string $id, string $type, - int|string $size, + int $size, bool $required, mixed $default, bool $signed, @@ -2539,7 +2531,7 @@ private function validateAttribute( '$id' => ID::custom($id), 'key' => $id, 'type' => $type, - 'size' => $this->getSize($type, $size), + 'size' => $size, 'required' => $required, 'default' => $default, 'signed' => $signed, @@ -2881,7 +2873,7 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de * @return Document * @throws Exception */ - public function updateAttribute(string $collection, string $id, ?string $type = null, int|string|null $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { $collectionDoc = $this->silent(fn () => $this->getCollection($collection)); @@ -2917,7 +2909,6 @@ public function updateAttribute(string $collection, string $id, ?string $type = || !\is_null($newKey); $type ??= $attribute->getAttribute('type'); $size ??= $attribute->getAttribute('size'); - $size = $this->getSize($type, $size); $signed ??= $attribute->getAttribute('signed'); $required ??= $attribute->getAttribute('required'); $default ??= $attribute->getAttribute('default'); @@ -2969,11 +2960,12 @@ public function updateAttribute(string $collection, string $id, ?string $type = } break; case self::VAR_BIGINT: - $sizeString = (string)$size; - $limit = ($this->adapter->getSupportForUnsignedBigInt() && !$signed) + $sizeString = BigIntHelper::normalizeUnsignedString((string)$size); + $limit = (!$signed && $this->adapter->getSupportForUnsignedBigInt()) ? BigIntHelper::UNSIGNED_MAX - : (string)$this->adapter->getLimitForBigInt(); - if (!BigIntHelper::fitsBigIntRange($sizeString, false, $this->adapter->getSupportForUnsignedBigInt() && !$signed)) { + : BigIntHelper::SIGNED_MAX; + + if (BigIntHelper::compareUnsignedStrings($sizeString, $limit) > 0) { throw new DatabaseException('Max size allowed for bigint is: ' . BigIntHelper::formatIntegerString($limit)); } break; @@ -3082,7 +3074,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = ->setAttribute('$id', $newKey ?? $id) ->setattribute('key', $newKey ?? $id) ->setAttribute('type', $type) - ->setAttribute('size', $this->getSize($type, $size)) + ->setAttribute('size', $size) ->setAttribute('signed', $signed) ->setAttribute('array', $array) ->setAttribute('format', $format) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 6cf55d35e..f740cab3e 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -301,7 +301,7 @@ public function deleteCollection(string $id): bool return $result; } - public function createAttribute(string $collection, string $id, string $type, int|string $size, bool $required, $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool { $result = $this->source->createAttribute( $collection, @@ -399,7 +399,7 @@ public function createAttributes(string $collection, array $attributes): bool return $result; } - public function updateAttribute(string $collection, string $id, ?string $type = null, int|string|null $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { $document = $this->source->updateAttribute( $collection, diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 5d1b71639..0e5eadb98 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -7,7 +7,6 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; -use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Validator; class Attribute extends Validator @@ -347,21 +346,6 @@ public function checkType(Document $attribute): bool break; case Database::VAR_BIGINT: - $sizeString = \is_string($size) ? $size : (string)$size; - if (!BigIntHelper::isIntegerString($sizeString, false)) { - $this->message = 'Size must be a positive integer'; - throw new DatabaseException($this->message); - } - - $sizeString = BigIntHelper::normalizeUnsignedString($sizeString); - $limit = (!$signed && $this->supportUnsignedBigInt) - ? BigIntHelper::UNSIGNED_MAX - : (string)$this->maxBigIntLength; - - if (BigIntHelper::compareUnsignedStrings($sizeString, $limit) > 0) { - $this->message = 'Max size allowed for bigint is: ' . BigIntHelper::formatIntegerString($limit); - throw new DatabaseException($this->message); - } break; case Database::VAR_FLOAT: diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 825c38ada..b745fd604 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2252,7 +2252,7 @@ public function testCreateAttributesBigIntIgnoresSizeLimit(): void $attrs = $collection->getAttribute('attributes'); $this->assertCount(1, $attrs); $this->assertEquals('foo', $attrs[0]['$id']); - $this->assertEquals((string)$size, (string)$attrs[0]['size']); + $this->assertEquals($size, $attrs[0]['size']); } public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadata(): void @@ -2298,8 +2298,8 @@ public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadat $this->assertNotNull($unsignedAttribute); $this->assertTrue($signedAttribute['signed']); $this->assertFalse($unsignedAttribute['signed']); - $this->assertEquals('0', (string)$signedAttribute['size']); - $this->assertEquals('0', (string)$unsignedAttribute['size']); + $this->assertEquals(0, $signedAttribute['size']); + $this->assertEquals(0, $unsignedAttribute['size']); // Signed overflow check (only when we can build an int beyond adapter signed max). $signedLimit = (int)$database->getAdapter()->getLimitForBigInt(); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 87026812e..2f54b7cad 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -161,8 +161,8 @@ public function testBigIntScenariosWithFiltering(): void $this->assertNotNull($signedAttr); $this->assertNotNull($unsignedAttr); - $this->assertSame('0', $signedAttr->getAttribute('size')); - $this->assertSame('0', $unsignedAttr->getAttribute('size')); + $this->assertSame(0, $signedAttr->getAttribute('size')); + $this->assertSame(0, $unsignedAttr->getAttribute('size')); // "Out of regular int limit" (32-bit) but valid bigint should still normalize to PHP int. $beyond32Bit = '2147483648'; From 946b8414f654f0539f4b24b7d7adb784a930d58a Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 21:27:19 +0530 Subject: [PATCH 24/29] updated --- tests/e2e/Adapter/Scopes/AttributeTests.php | 2 +- tests/e2e/Adapter/Scopes/DocumentTests.php | 23 --------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index b745fd604..85fb0211c 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2317,7 +2317,7 @@ public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadat $largeUnsignedAttribute = [[ '$id' => 'unsigned_bigint_large', 'type' => Database::VAR_BIGINT, - 'size' => '18446744073709551615', + 'size' => 0, 'required' => false, 'signed' => false, ]]; diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 2f54b7cad..2ba8ac028 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -7922,27 +7922,4 @@ public function testRegexInjection(): void // $database->deleteCollection($collectionName); // } - public function testCreateDocumentWithBigIntStringValues(): void - { - /** @var Database $database */ - $database = $this->getDatabase(); - - $collection = 'test_big_int_string_values'; - $database->createCollection($collection); - $this->assertEquals(true, $database->createAttribute($collection, 'bigint_unsigned', Database::VAR_BIGINT, 0, false, signed: false)); - - $database->createDocument($collection, new Document([ - '$id' => 'bigint-string-type-doc', - '$permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - ], - 'bigint_unsigned' => (string)9223372036854775807, - ])); - - $data = $database->getDocument($collection, 'bigint-string-type-doc'); - $this->assertFalse($data->isEmpty()); - $this->assertEquals('bigint-string-type-doc', $data->getId()); - $this->assertEquals('9223372036854775807', (string)$data->getAttribute('bigint_unsigned')); - } } From fafacee17b537bd405ab858c398174ad8d16e947 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 21:35:50 +0530 Subject: [PATCH 25/29] updated --- src/Database/Database.php | 40 --------------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 604b63840..7905bc884 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1906,7 +1906,6 @@ public function updateCollection(string $id, array $permissions, bool $documentS public function getCollection(string $id): Document { $collection = $this->silent(fn () => $this->getDocument(self::METADATA, $id)); - $collection = $this->normalizeCollectionAttributeSizes($collection); if ( $id !== self::METADATA @@ -1942,8 +1941,6 @@ public function listCollections(int $limit = 25, int $offset = 0): array Query::offset($offset) ])); - $result = \array_map(fn (Document $collection) => $this->normalizeCollectionAttributeSizes($collection), $result); - try { $this->trigger(self::EVENT_COLLECTION_LIST, $result); } catch (\Throwable $e) { @@ -1953,43 +1950,6 @@ public function listCollections(int $limit = 25, int $offset = 0): array return $result; } - private function normalizeCollectionAttributeSizes(Document $collection): Document - { - if ($collection->isEmpty()) { - return $collection; - } - - $attributes = $collection->getAttribute('attributes', []); - if (!\is_array($attributes)) { - return $collection; - } - - foreach ($attributes as $attribute) { - if (!$attribute instanceof Document) { - continue; - } - - $type = $attribute->getAttribute('type'); - - if ($type === self::VAR_BIGINT) { - $formatOptions = $attribute->getAttribute('formatOptions', []); - if (\is_array($formatOptions)) { - if (\array_key_exists('min', $formatOptions) && $formatOptions['min'] !== null) { - $formatOptions['min'] = (string)$formatOptions['min']; - } - if (\array_key_exists('max', $formatOptions) && $formatOptions['max'] !== null) { - $formatOptions['max'] = (string)$formatOptions['max']; - } - $attribute->setAttribute('formatOptions', $formatOptions); - } - } - } - - $collection->setAttribute('attributes', $attributes); - - return $collection; - } - /** * Get Collection Size * From fe3bf721da71693f064b2de5fec2a4943b938892 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 21:39:51 +0530 Subject: [PATCH 26/29] updated --- tests/e2e/Adapter/Scopes/AttributeTests.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 85fb0211c..c0a0c84da 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2313,7 +2313,7 @@ public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadat } } - // Unsigned > signed-max behavior validated through batch create (string size avoids int overflow). + // Batch create with unsigned bigint should be accepted with default size semantics. $largeUnsignedAttribute = [[ '$id' => 'unsigned_bigint_large', 'type' => Database::VAR_BIGINT, @@ -2321,18 +2321,7 @@ public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadat 'required' => false, 'signed' => false, ]]; - - if ($database->getAdapter()->getSupportForUnsignedBigInt()) { - $this->assertTrue($database->createAttributes($collectionName, $largeUnsignedAttribute)); - } else { - try { - $database->createAttributes($collectionName, $largeUnsignedAttribute); - $this->fail('Expected DatabaseException for unsigned bigint overflow on adapter without unsigned bigint support'); - } catch (\Throwable $e) { - $this->assertInstanceOf(DatabaseException::class, $e); - $this->assertStringContainsString('Max size allowed for bigint', $e->getMessage()); - } - } + $this->assertTrue($database->createAttributes($collectionName, $largeUnsignedAttribute)); } public function testCreateAttributesSuccessMultiple(): void From d3282bce130bfe7d956a491e91d8340aa8631956 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Wed, 8 Apr 2026 21:40:56 +0530 Subject: [PATCH 27/29] updated --- tests/e2e/Adapter/Scopes/AttributeTests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index c0a0c84da..11c36d6e5 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2313,13 +2313,13 @@ public function testCreateAttributesBigIntValidationSignedUnsignedAndSizeMetadat } } - // Batch create with unsigned bigint should be accepted with default size semantics. $largeUnsignedAttribute = [[ '$id' => 'unsigned_bigint_large', 'type' => Database::VAR_BIGINT, 'size' => 0, 'required' => false, 'signed' => false, + 'default' => '18446744073709551615' ]]; $this->assertTrue($database->createAttributes($collectionName, $largeUnsignedAttribute)); } From 17615116013c18b8f4d00246d52268ffbc15b11c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 10:52:46 +0530 Subject: [PATCH 28/29] updated the type for big int validator --- src/Database/Validator/BigInt.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Database/Validator/BigInt.php b/src/Database/Validator/BigInt.php index 2dd1332c1..61cdc4bc7 100644 --- a/src/Database/Validator/BigInt.php +++ b/src/Database/Validator/BigInt.php @@ -2,6 +2,7 @@ namespace Utopia\Database\Validator; +use Utopia\Database\Database; use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Validator; @@ -33,7 +34,7 @@ public function isArray(): bool public function getType(): string { - return self::TYPE_MIXED; + return Database::VAR_BIGINT; } public function isValid(mixed $value): bool From 0e43fd44f2190da36cac816a17650d697e034507 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 9 Apr 2026 16:35:50 +0530 Subject: [PATCH 29/29] updated --- src/Database/Database.php | 16 ++-- src/Database/Helpers/BigInt.php | 93 ----------------------- src/Database/Validator/BigInt.php | 98 +++++++++++++++++++++++-- src/Database/Validator/Query/Filter.php | 2 +- src/Database/Validator/Structure.php | 5 +- 5 files changed, 103 insertions(+), 111 deletions(-) delete mode 100644 src/Database/Helpers/BigInt.php diff --git a/src/Database/Database.php b/src/Database/Database.php index 7905bc884..0860b0966 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -22,13 +22,13 @@ use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Type as TypeException; -use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Attribute as AttributeValidator; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization\Input; +use Utopia\Database\Validator\BigInt as BigIntValidator; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator; use Utopia\Database\Validator\PartialStructure; @@ -2593,7 +2593,7 @@ protected function validateDefaultTypes(string $type, mixed $default): void } break; case Database::VAR_BIGINT: - if ($defaultType !== 'integer') { + if ($defaultType !== 'integer' && $defaultType !== 'string') { throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type); } break; @@ -2920,13 +2920,13 @@ public function updateAttribute(string $collection, string $id, ?string $type = } break; case self::VAR_BIGINT: - $sizeString = BigIntHelper::normalizeUnsignedString((string)$size); + $sizeString = BigIntValidator::normalizeUnsignedString((string)$size); $limit = (!$signed && $this->adapter->getSupportForUnsignedBigInt()) - ? BigIntHelper::UNSIGNED_MAX - : BigIntHelper::SIGNED_MAX; + ? BigIntValidator::UNSIGNED_MAX + : BigIntValidator::SIGNED_MAX; - if (BigIntHelper::compareUnsignedStrings($sizeString, $limit) > 0) { - throw new DatabaseException('Max size allowed for bigint is: ' . BigIntHelper::formatIntegerString($limit)); + if (BigIntValidator::compareUnsignedStrings($sizeString, $limit) > 0) { + throw new DatabaseException('Max size allowed for bigint is: ' . BigIntValidator::formatIntegerString($limit)); } break; case self::VAR_FLOAT: @@ -8969,7 +8969,7 @@ public function casting(Document $collection, Document $document): Document $node = (int)$node; break; case self::VAR_BIGINT: - if (\is_string($node) && BigIntHelper::fitsPhpInt($node, $signed)) { + if (\is_string($node) && BigIntValidator::fitsPhpInt($node, $signed)) { $node = (int)$node; } break; diff --git a/src/Database/Helpers/BigInt.php b/src/Database/Helpers/BigInt.php deleted file mode 100644 index 06a9a0453..000000000 --- a/src/Database/Helpers/BigInt.php +++ /dev/null @@ -1,93 +0,0 @@ - $lenB) { - return 1; - } - if ($a === $b) { - return 0; - } - - return $a < $b ? -1 : 1; - } - - public static function formatIntegerString(string $value): string - { - $negative = \str_starts_with($value, '-'); - if ($negative) { - $value = \substr($value, 1); - } - - $value = self::normalizeUnsignedString($value); - $formatted = \preg_replace('/\B(?=(\d{3})+(?!\d))/', ',', $value) ?? $value; - - return $negative ? "-{$formatted}" : $formatted; - } -} diff --git a/src/Database/Validator/BigInt.php b/src/Database/Validator/BigInt.php index 61cdc4bc7..fc560694a 100644 --- a/src/Database/Validator/BigInt.php +++ b/src/Database/Validator/BigInt.php @@ -3,11 +3,14 @@ namespace Utopia\Database\Validator; use Utopia\Database\Database; -use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Validator; class BigInt extends Validator { + public const SIGNED_MIN = '-9223372036854775808'; + public const SIGNED_MAX = '9223372036854775807'; + public const UNSIGNED_MAX = '18446744073709551615'; + public function __construct( private readonly bool $signed, private readonly bool $supportUnsigned64Bit = true @@ -18,13 +21,13 @@ public function getDescription(): string { if ($this->signed) { return 'Value must be a valid signed 64-bit integer between ' . - BigIntHelper::formatIntegerString(BigIntHelper::SIGNED_MIN) . - ' and ' . BigIntHelper::formatIntegerString(BigIntHelper::SIGNED_MAX); + self::formatIntegerString(self::SIGNED_MIN) . + ' and ' . self::formatIntegerString(self::SIGNED_MAX); } - $max = $this->supportUnsigned64Bit ? BigIntHelper::UNSIGNED_MAX : BigIntHelper::SIGNED_MAX; + $max = $this->supportUnsigned64Bit ? self::UNSIGNED_MAX : self::SIGNED_MAX; return 'Value must be a valid unsigned 64-bit integer between 0 and ' . - BigIntHelper::formatIntegerString($max); + self::formatIntegerString($max); } public function isArray(): bool @@ -47,6 +50,89 @@ public function isValid(mixed $value): bool return false; } - return BigIntHelper::fitsBigIntRange($value, $this->signed, $this->supportUnsigned64Bit); + return self::fitsBigIntRange($value, $this->signed, $this->supportUnsigned64Bit); + } + + public static function isIntegerString(string $value, bool $signed = true): bool + { + return \preg_match($signed ? '/^-?\d+$/' : '/^\d+$/', $value) === 1; + } + + public static function fitsPhpInt(string $value, bool $signed = true): bool + { + if (!self::isIntegerString($value, $signed)) { + return false; + } + + $phpMax = (string)\PHP_INT_MAX; + $phpMinAbs = \ltrim((string)\PHP_INT_MIN, '-'); + + if ($signed && \str_starts_with($value, '-')) { + $digits = self::normalizeUnsignedString(\substr($value, 1)); + return self::compareUnsignedStrings($digits, $phpMinAbs) <= 0; + } + + $digits = self::normalizeUnsignedString($value); + return self::compareUnsignedStrings($digits, $phpMax) <= 0; + } + + public static function fitsBigIntRange(string $value, bool $signed, bool $supportUnsigned64Bit = true): bool + { + if (!self::isIntegerString($value, $signed)) { + return false; + } + + if ($signed) { + if (\str_starts_with($value, '-')) { + $digits = self::normalizeUnsignedString(\substr($value, 1)); + $minAbs = \ltrim(\str_replace('-', '', self::SIGNED_MIN), '0'); + return self::compareUnsignedStrings($digits, $minAbs) <= 0; + } + + return self::compareUnsignedStrings($value, self::SIGNED_MAX) <= 0; + } + + $max = $supportUnsigned64Bit ? self::UNSIGNED_MAX : self::SIGNED_MAX; + return self::compareUnsignedStrings($value, $max) <= 0; + } + + public static function normalizeUnsignedString(string $value): string + { + $value = \trim($value); + $value = \ltrim($value, '0'); + return $value === '' ? '0' : $value; + } + + public static function compareUnsignedStrings(string $a, string $b): int + { + $a = self::normalizeUnsignedString($a); + $b = self::normalizeUnsignedString($b); + + $lenA = \strlen($a); + $lenB = \strlen($b); + if ($lenA < $lenB) { + return -1; + } + if ($lenA > $lenB) { + return 1; + } + if ($a === $b) { + return 0; + } + + return $a < $b ? -1 : 1; + } + + public static function formatIntegerString(string $value): string + { + $negative = \str_starts_with($value, '-'); + if ($negative) { + $value = \substr($value, 1); + } + + $value = self::normalizeUnsignedString($value); + $formatted = \preg_replace('/\B(?=(\d{3})+(?!\d))/', ',', $value) ?? $value; + + return $negative ? "-{$formatted}" : $formatted; } } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 232a6a32c..b2c918d75 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -157,7 +157,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s case Database::VAR_INTEGER: $size = $attributeSchema['size'] ?? 4; $signed = $attributeSchema['signed'] ?? true; - $bits = ($attributeType === Database::VAR_BIGINT || $size >= 8) ? 64 : 32; + $bits = $size >= 8 ? 64 : 32; // For 64-bit unsigned, use signed since PHP doesn't support true 64-bit unsigned $unsigned = !$signed && $bits < 64; $validator = new Integer(false, $bits, $unsigned); diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 1609675f9..84abaa772 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -7,7 +7,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; -use Utopia\Database\Helpers\BigInt as BigIntHelper; use Utopia\Database\Operator; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Operator as OperatorValidator; @@ -365,7 +364,7 @@ protected function checkForInvalidAttributeValues(Document $document, array $str case Database::VAR_INTEGER: // Determine bit size based on attribute size in bytes // BIGINT is always 64-bit in SQL adapters; VAR_INTEGER uses size to decide. - $bits = ($type === Database::VAR_BIGINT || $size >= 8) ? 64 : 32; + $bits = $size >= 8 ? 64 : 32; // For 64-bit unsigned, use signed since PHP doesn't support true 64-bit unsigned // The Range validator will restrict to positive values only $unsigned = !$signed && $bits < 64; @@ -464,7 +463,7 @@ protected function checkForInvalidAttributeValues(Document $document, array $str public function isBigIntStringWithinPhpIntRange(string $value, bool $signed): bool { - return BigIntHelper::fitsPhpInt($value, $signed); + return BigInt::fitsPhpInt($value, $signed); } /**