diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index a7b385cce..39b96d7fb 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..0faaa8622 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1734,6 +1734,10 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool 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 +1752,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 7ddde43d3..2f90e11d5 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1319,6 +1319,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: @@ -2220,6 +2221,7 @@ private function getMongoTypeCode(string $appwriteType): string Database::VAR_MEDIUMTEXT => 'string', Database::VAR_LONGTEXT => 'string', Database::VAR_INTEGER => 'int', + Database::VAR_BIGINT => 'long', Database::VAR_FLOAT => 'double', Database::VAR_BOOLEAN => 'bool', Database::VAR_DATETIME => 'date', @@ -3013,6 +3015,16 @@ public function getLimitForInt(): int return 4294967295; } + /** + * Get max BIGINT limit + * + * @return int + */ + public function getLimitForBigInt(): int + { + return Database::MAX_BIG_INT; + } + /** * 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 668753387..ddf90c09b 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -333,6 +333,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..7362dc265 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1972,6 +1972,9 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return 'INTEGER'; + case Database::VAR_BIGINT: + return 'BIGINT'; + case Database::VAR_FLOAT: return 'DOUBLE PRECISION'; @@ -2000,7 +2003,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 6864e6aee..baca6b4e7 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -896,6 +896,16 @@ public function getLimitForInt(): int return 4294967295; } + /** + * Get max BIGINT limit + * + * @return int + */ + public function getLimitForBigInt(): int + { + return Database::MAX_BIG_INT; + } + /** * Get maximum column limit. * https://mariadb.com/kb/en/innodb-limitations/#limitations-on-schema @@ -1164,6 +1174,10 @@ public function getAttributeWidth(Document $collection): int } break; + case Database::VAR_BIGINT: + $total += 8; // BIGINT 8 bytes + break; + case Database::VAR_FLOAT: $total += 8; // DOUBLE 8 bytes break; diff --git a/src/Database/Database.php b/src/Database/Database.php index ac58d72f0..44ea8013b 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(), @@ -2588,6 +2590,11 @@ protected function validateDefaultTypes(string $type, mixed $default): void 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); @@ -2607,6 +2614,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, @@ -2909,6 +2917,8 @@ 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: + break; case self::VAR_FLOAT: case self::VAR_BOOLEAN: case self::VAR_DATETIME: @@ -2975,6 +2985,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, @@ -7423,6 +7434,7 @@ public function increaseDocumentAttribute( $whiteList = [ self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT ]; @@ -7521,6 +7533,7 @@ public function decreaseDocumentAttribute( $whiteList = [ self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT ]; @@ -8932,6 +8945,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/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 021a85d97..a924f189b 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,9 @@ public function checkType(Document $attribute): bool } break; + case Database::VAR_BIGINT: + break; + case Database::VAR_FLOAT: case Database::VAR_BOOLEAN: case Database::VAR_DATETIME: @@ -420,6 +430,8 @@ public function checkType(Document $attribute): bool Database::VAR_MEDIUMTEXT, Database::VAR_LONGTEXT, Database::VAR_INTEGER, + Database::VAR_BIGINT, + Database::VAR_BIGINT, Database::VAR_FLOAT, Database::VAR_BOOLEAN, Database::VAR_DATETIME, @@ -522,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; @@ -543,6 +561,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, 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; diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index bf376d101..3c5633734 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2221,6 +2221,39 @@ public function testCreateAttributesIntegerSizeLimit(): void } } + + public function testCreateAttributesBigInt(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + if (!$database->getAdapter()->getSupportForBatchCreateAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $database->createCollection(__FUNCTION__); + + $limit = $database->getAdapter()->getLimitForBigInt() / 2; + $size = (int)$limit + 1; + + $attributes = [[ + '$id' => 'foo', + 'type' => Database::VAR_BIGINT, + 'size' => $size, + 'required' => false + ]]; + + $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 { /** @var Database $database */ diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index d16004d32..673f57409 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 */ @@ -1467,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 */ diff --git a/tests/unit/Validator/AttributeTest.php b/tests/unit/Validator/AttributeTest.php index 2f7303cd1..fd82c1fd8 100644 --- a/tests/unit/Validator/AttributeTest.php +++ b/tests/unit/Validator/AttributeTest.php @@ -1139,6 +1139,56 @@ public function testUnsignedIntegerSizeTooLarge(): void $validator->isValid($attribute); } + public function testBigIntSizeNotLimited(): 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->assertTrue($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(