diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index a7b385cce..6e5dec54b 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. * @@ -1442,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 223f91e71..e45e9f6ac 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); } } @@ -2251,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/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/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 ac58d72f0..0860b0966 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -28,6 +28,7 @@ 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; @@ -42,6 +43,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 +2514,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(), @@ -2521,6 +2524,7 @@ private function validateAttribute( filterCallback: fn ($id) => $this->adapter->filter($id), isMigrating: $this->isMigrating(), sharedTables: $this->getSharedTables(), + supportUnsignedBigInt: $this->adapter->getSupportForUnsignedBigInt(), ); $validator->isValid($attribute); @@ -2588,6 +2592,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' && $defaultType !== 'string') { + 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 +2616,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 +2919,16 @@ 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: + $sizeString = BigIntValidator::normalizeUnsignedString((string)$size); + $limit = (!$signed && $this->adapter->getSupportForUnsignedBigInt()) + ? BigIntValidator::UNSIGNED_MAX + : BigIntValidator::SIGNED_MAX; + + if (BigIntValidator::compareUnsignedStrings($sizeString, $limit) > 0) { + throw new DatabaseException('Max size allowed for bigint is: ' . BigIntValidator::formatIntegerString($limit)); + } + break; case self::VAR_FLOAT: case self::VAR_BOOLEAN: case self::VAR_DATETIME: @@ -2975,6 +2995,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, @@ -3137,7 +3158,7 @@ public function updateAttribute(string $collection, string $id, ?string $type = $collection, $newKey ?? $id, $originalType, - $originalSize, + (int)$originalSize, $originalSigned, $originalArray, $originalKey, @@ -5577,7 +5598,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()); @@ -5685,7 +5708,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()); @@ -6257,7 +6282,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()); @@ -6378,7 +6404,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)) { @@ -6423,7 +6450,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)) { @@ -7269,7 +7297,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)) { @@ -7423,6 +7452,7 @@ public function increaseDocumentAttribute( $whiteList = [ self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT ]; @@ -7521,6 +7551,7 @@ public function decreaseDocumentAttribute( $whiteList = [ self::VAR_INTEGER, + self::VAR_BIGINT, self::VAR_FLOAT ]; @@ -8084,7 +8115,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)) { @@ -8306,7 +8338,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()); @@ -8562,7 +8595,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()); @@ -8635,7 +8669,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()); @@ -8900,6 +8935,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)) { @@ -8932,6 +8968,11 @@ public function casting(Document $collection, Document $document): Document case self::VAR_INTEGER: $node = (int)$node; break; + case self::VAR_BIGINT: + if (\is_string($node) && BigIntValidator::fitsPhpInt($node, $signed)) { + $node = (int)$node; + } + break; case self::VAR_FLOAT: $node = (float)$node; break; @@ -8948,7 +8989,6 @@ public function casting(Document $collection, Document $document): Document return $document; } - /** * Encode Attribute * diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 021a85d97..0e5eadb98 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, @@ -58,7 +60,13 @@ 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) { + $this->maxBigIntLength = $this->maxIntLength; + } + foreach ($attributes as $attribute) { $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); $this->attributes[$key] = $attribute; @@ -337,6 +345,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 +431,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 +535,12 @@ protected function validateDefaultTypes(string $type, mixed $default): void throw new DatabaseException($this->message); } break; + case Database::VAR_BIGINT: + if ($defaultType !== 'integer' && $defaultType !== 'string') { + $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 +562,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/BigInt.php b/src/Database/Validator/BigInt.php new file mode 100644 index 000000000..fc560694a --- /dev/null +++ b/src/Database/Validator/BigInt.php @@ -0,0 +1,138 @@ +signed) { + return 'Value must be a valid signed 64-bit integer between ' . + self::formatIntegerString(self::SIGNED_MIN) . + ' and ' . self::formatIntegerString(self::SIGNED_MAX); + } + + $max = $this->supportUnsigned64Bit ? self::UNSIGNED_MAX : self::SIGNED_MAX; + return 'Value must be a valid unsigned 64-bit integer between 0 and ' . + self::formatIntegerString($max); + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return Database::VAR_BIGINT; + } + + 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 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/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 dd07e44c8..b2c918d75 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(); @@ -161,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 a65734dbd..84abaa772 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -109,7 +109,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 +238,7 @@ public function isValid($document): bool return false; } - if (!$this->checkForInvalidAttributeValues($structure, $keys)) { + if (!$this->checkForInvalidAttributeValues($document, $structure, $keys)) { return false; } @@ -305,7 +306,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 +337,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) { @@ -353,16 +363,21 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) case Database::VAR_INTEGER: // 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 = $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; + 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(); @@ -446,6 +461,11 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) return true; } + public function isBigIntStringWithinPhpIntRange(string $value, bool $signed): bool + { + return BigInt::fitsPhpInt($value, $signed); + } + /** * Is array * diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index bf376d101..11c36d6e5 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -2221,6 +2221,109 @@ public function testCreateAttributesIntegerSizeLimit(): void } } + + public function testCreateAttributesBigIntIgnoresSizeLimit(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + if (!$database->getAdapter()->getSupportForBatchCreateAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collectionName = 'bigint_ignores_size_limit'; + $database->createCollection($collectionName); + + $limit = $database->getAdapter()->getLimitForBigInt() / 2; + $size = (int)$limit + 1; + + $attributes = [[ + '$id' => 'foo', + 'type' => Database::VAR_BIGINT, + 'size' => $size, + 'required' => false + ]]; + + $result = $database->createAttributes($collectionName, $attributes); + $this->assertTrue($result); + + $collection = $database->getCollection($collectionName); + $attrs = $collection->getAttribute('attributes'); + $this->assertCount(1, $attrs); + $this->assertEquals('foo', $attrs[0]['$id']); + $this->assertEquals($size, $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, $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(); + 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()); + } + } + + $largeUnsignedAttribute = [[ + '$id' => 'unsigned_bigint_large', + 'type' => Database::VAR_BIGINT, + 'size' => 0, + 'required' => false, + 'signed' => false, + 'default' => '18446744073709551615' + ]]; + $this->assertTrue($database->createAttributes($collectionName, $largeUnsignedAttribute)); + } + 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..2ba8ac028 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -103,6 +103,166 @@ 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 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'))); + + // 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, + ])); + $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 = \PHP_INT_MIN; + $signedMax = \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 */ @@ -1467,6 +1627,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 */ @@ -7722,4 +7921,5 @@ public function testRegexInjection(): void // } // $database->deleteCollection($collectionName); // } + } 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( 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() + ); + } + }