diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 7b4de1463..b189c6b7a 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -899,6 +899,13 @@ abstract public function getLimitForIndexes(): int; */ abstract public function getMaxIndexLength(): int; + /** + * Get the maximum VARCHAR length for this adapter + * + * @return int + */ + abstract public function getMaxVarcharLength(): int; + /** * Get the maximum UID length for this adapter * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index af1e0badf..71927fded 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1678,6 +1678,24 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return "VARCHAR({$size})"; + case Database::VAR_VARCHAR: + if ($size <= 0) { + throw new DatabaseException('VARCHAR size ' . $size . ' is invalid; must be > 0. Use TEXT, MEDIUMTEXT, or LONGTEXT instead.'); + } + if ($size > $this->getMaxVarcharLength()) { + throw new DatabaseException('VARCHAR size ' . $size . ' exceeds maximum varchar length ' . $this->getMaxVarcharLength() . '. Use TEXT, MEDIUMTEXT, or LONGTEXT instead.'); + } + return "VARCHAR({$size})"; + + case Database::VAR_TEXT: + return 'TEXT'; + + case Database::VAR_MEDIUMTEXT: + return 'MEDIUMTEXT'; + + case Database::VAR_LONGTEXT: + return 'LONGTEXT'; + case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554 $signed = ($signed) ? '' : ' UNSIGNED'; @@ -1701,7 +1719,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_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_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 7e4e0ba61..6b6e4e986 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2067,6 +2067,10 @@ private function getMongoTypeCode(string $appwriteType): string { return match ($appwriteType) { Database::VAR_STRING => 'string', + Database::VAR_VARCHAR => 'string', + Database::VAR_TEXT => 'string', + Database::VAR_MEDIUMTEXT => 'string', + Database::VAR_LONGTEXT => 'string', Database::VAR_INTEGER => 'int', Database::VAR_FLOAT => 'double', Database::VAR_BOOLEAN => 'bool', @@ -2694,6 +2698,17 @@ public function getLimitForString(): int return 2147483647; } + /** + * Get max VARCHAR limit + * MongoDB doesn't distinguish between string types, so using same as string limit + * + * @return int + */ + public function getMaxVarcharLength(): int + { + return 2147483647; + } + /** * Get max INT limit * diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php index 71d59c98a..263c8a183 100644 --- a/src/Database/Adapter/Pool.php +++ b/src/Database/Adapter/Pool.php @@ -287,6 +287,11 @@ public function getMaxIndexLength(): int return $this->delegate(__FUNCTION__, \func_get_args()); } + public function getMaxVarcharLength(): int + { + return $this->delegate(__FUNCTION__, \func_get_args()); + } + public function getMaxUIDLength(): int { return $this->delegate(__FUNCTION__, \func_get_args()); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 94a8b61f7..a611f1038 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1922,6 +1922,14 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool return "VARCHAR({$size})"; + case Database::VAR_VARCHAR: + return "VARCHAR({$size})"; + + case Database::VAR_TEXT: + case Database::VAR_MEDIUMTEXT: + case Database::VAR_LONGTEXT: + 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 @@ -1958,7 +1966,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_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_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 8f0e04718..4a7c34520 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1144,6 +1144,19 @@ public function getAttributeWidth(Document $collection): int break; + case Database::VAR_VARCHAR: + $total += match (true) { + $attribute['size'] > 255 => $attribute['size'] * 4 + 2, // VARCHAR(>255) + 2 length + default => $attribute['size'] * 4 + 1, // VARCHAR(<=255) + 1 length + }; + break; + + case Database::VAR_TEXT: + case Database::VAR_MEDIUMTEXT: + case Database::VAR_LONGTEXT: + $total += 20; // Pointer storage for TEXT types + break; + case Database::VAR_INTEGER: if ($attribute['size'] >= 8) { $total += 8; // BIGINT 8 bytes diff --git a/src/Database/Database.php b/src/Database/Database.php index a715fd56b..b9b52a8c5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -25,6 +25,7 @@ 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\Index as IndexValidator; @@ -45,6 +46,11 @@ class Database public const VAR_BOOLEAN = 'boolean'; public const VAR_DATETIME = 'datetime'; + public const VAR_VARCHAR = 'varchar'; + public const VAR_TEXT = 'text'; + public const VAR_MEDIUMTEXT = 'mediumtext'; + public const VAR_LONGTEXT = 'longtext'; + // ID types public const VAR_ID = 'id'; public const VAR_UUID7 = 'uuid7'; @@ -2232,36 +2238,6 @@ private function validateAttribute( array $formatOptions, array $filters ): Document { - // Attribute IDs are case-insensitive - $attributes = $collection->getAttribute('attributes', []); - - /** @var array $attributes */ - foreach ($attributes as $attribute) { - if (\strtolower($attribute->getId()) === \strtolower($id)) { - throw new DuplicateException('Attribute already exists in metadata'); - } - } - - if ($this->adapter->getSupportForSchemaAttributes() && !($this->getSharedTables() && $this->isMigrating())) { - $schema = $this->getSchemaAttributes($collection->getId()); - foreach ($schema as $attribute) { - $newId = $this->adapter->filter($attribute->getId()); - if (\strtolower($newId) === \strtolower($id)) { - throw new DuplicateException('Attribute already exists in schema'); - } - } - } - - // Ensure required filters for the attribute are passed - $requiredFilters = $this->getRequiredFilters($type); - if (!empty(\array_diff($requiredFilters, $filters))) { - throw new DatabaseException("Attribute of type: $type requires the following filters: " . implode(",", $requiredFilters)); - } - - if ($format && !Structure::hasFormat($format, $type)) { - throw new DatabaseException('Format ("' . $format . '") not available for this attribute type ("' . $type . '")'); - } - $attribute = new Document([ '$id' => ID::custom($id), 'key' => $id, @@ -2276,111 +2252,31 @@ private function validateAttribute( 'filters' => $filters, ]); - $this->checkAttribute($collection, $attribute); - - switch ($type) { - case self::VAR_ID: - - break; - case self::VAR_STRING: - if ($size > $this->adapter->getLimitForString()) { - throw new DatabaseException('Max size allowed for string is: ' . number_format($this->adapter->getLimitForString())); - } - break; - case self::VAR_INTEGER: - $limit = ($signed) ? $this->adapter->getLimitForInt() / 2 : $this->adapter->getLimitForInt(); - if ($size > $limit) { - throw new DatabaseException('Max size allowed for int is: ' . number_format($limit)); - } - break; - case self::VAR_FLOAT: - case self::VAR_BOOLEAN: - case self::VAR_DATETIME: - case self::VAR_RELATIONSHIP: - break; - case self::VAR_OBJECT: - if (!$this->adapter->getSupportForObject()) { - throw new DatabaseException('Object attributes are not supported'); - } - if (!empty($size)) { - throw new DatabaseException('Size must be empty for object attributes'); - } - if (!empty($array)) { - throw new DatabaseException('Object attributes cannot be arrays'); - } - break; - case self::VAR_POINT: - case self::VAR_LINESTRING: - case self::VAR_POLYGON: - // Check if adapter supports spatial attributes - if (!$this->adapter->getSupportForSpatialAttributes()) { - throw new DatabaseException('Spatial attributes are not supported'); - } - if (!empty($size)) { - throw new DatabaseException('Size must be empty for spatial attributes'); - } - if (!empty($array)) { - throw new DatabaseException('Spatial attributes cannot be arrays'); - } - break; - case self::VAR_VECTOR: - if (!$this->adapter->getSupportForVectors()) { - throw new DatabaseException('Vector types are not supported by the current database'); - } - if ($array) { - throw new DatabaseException('Vector type cannot be an array'); - } - if ($size <= 0) { - throw new DatabaseException('Vector dimensions must be a positive integer'); - } - if ($size > self::MAX_VECTOR_DIMENSIONS) { - throw new DatabaseException('Vector dimensions cannot exceed ' . self::MAX_VECTOR_DIMENSIONS); - } - - // Validate default value if provided - if ($default !== null) { - if (!is_array($default)) { - throw new DatabaseException('Vector default value must be an array'); - } - if (count($default) !== $size) { - throw new DatabaseException('Vector default value must have exactly ' . $size . ' elements'); - } - foreach ($default as $component) { - if (!is_numeric($component)) { - throw new DatabaseException('Vector default value must contain only numeric elements'); - } - } - } - break; - default: - $supportedTypes = [ - self::VAR_STRING, - self::VAR_INTEGER, - self::VAR_FLOAT, - self::VAR_BOOLEAN, - self::VAR_DATETIME, - self::VAR_RELATIONSHIP - ]; - if ($this->adapter->getSupportForVectors()) { - $supportedTypes[] = self::VAR_VECTOR; - } - if ($this->adapter->getSupportForSpatialAttributes()) { - \array_push($supportedTypes, ...self::SPATIAL_TYPES); - } - if ($this->adapter->getSupportForObject()) { - $supportedTypes[] = self::VAR_OBJECT; - } - throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes)); - } - - // Only execute when $default is given - if (!\is_null($default)) { - if ($required === true) { - throw new DatabaseException('Cannot set a default value for a required attribute'); - } + $collectionClone = clone $collection; + $collectionClone->setAttribute('attributes', $attribute, Document::SET_TYPE_APPEND); + + $validator = new AttributeValidator( + attributes: $collection->getAttribute('attributes', []), + schemaAttributes: $this->adapter->getSupportForSchemaAttributes() + ? $this->getSchemaAttributes($collection->getId()) + : [], + maxAttributes: $this->adapter->getLimitForAttributes(), + maxWidth: $this->adapter->getDocumentSizeLimit(), + maxStringLength: $this->adapter->getLimitForString(), + maxVarcharLength: $this->adapter->getMaxVarcharLength(), + maxIntLength: $this->adapter->getLimitForInt(), + supportForSchemaAttributes: $this->adapter->getSupportForSchemaAttributes(), + supportForVectors: $this->adapter->getSupportForVectors(), + supportForSpatialAttributes: $this->adapter->getSupportForSpatialAttributes(), + supportForObject: $this->adapter->getSupportForObject(), + attributeCountCallback: fn () => $this->adapter->getCountOfAttributes($collectionClone), + attributeWidthCallback: fn () => $this->adapter->getAttributeWidth($collectionClone), + filterCallback: fn ($id) => $this->adapter->filter($id), + isMigrating: $this->isMigrating(), + sharedTables: $this->getSharedTables(), + ); - $this->validateDefaultTypes($type, $default); - } + $validator->isValid($attribute); return $attribute; } @@ -2430,6 +2326,14 @@ protected function validateDefaultTypes(string $type, mixed $default): void switch ($type) { case self::VAR_STRING: + case self::VAR_VARCHAR: + case self::VAR_TEXT: + case self::VAR_MEDIUMTEXT: + case self::VAR_LONGTEXT: + if ($defaultType !== 'string') { + throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type); + } + break; case self::VAR_INTEGER: case self::VAR_FLOAT: case self::VAR_BOOLEAN: @@ -2451,6 +2355,10 @@ protected function validateDefaultTypes(string $type, mixed $default): void default: $supportedTypes = [ self::VAR_STRING, + self::VAR_VARCHAR, + self::VAR_TEXT, + self::VAR_MEDIUMTEXT, + self::VAR_LONGTEXT, self::VAR_INTEGER, self::VAR_FLOAT, self::VAR_BOOLEAN, diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php new file mode 100644 index 000000000..021a85d97 --- /dev/null +++ b/src/Database/Validator/Attribute.php @@ -0,0 +1,561 @@ + $attributes + */ + protected array $attributes = []; + + /** + * @var array $schemaAttributes + */ + protected array $schemaAttributes = []; + + /** + * @param array $attributes + * @param array $schemaAttributes + * @param int $maxAttributes + * @param int $maxWidth + * @param int $maxStringLength + * @param int $maxVarcharLength + * @param int $maxIntLength + * @param bool $supportForSchemaAttributes + * @param bool $supportForVectors + * @param bool $supportForSpatialAttributes + * @param bool $supportForObject + * @param callable|null $attributeCountCallback + * @param callable|null $attributeWidthCallback + * @param callable|null $filterCallback + * @param bool $isMigrating + * @param bool $sharedTables + */ + public function __construct( + array $attributes, + array $schemaAttributes = [], + protected int $maxAttributes = 0, + protected int $maxWidth = 0, + protected int $maxStringLength = 0, + protected int $maxVarcharLength = 0, + protected int $maxIntLength = 0, + protected bool $supportForSchemaAttributes = false, + protected bool $supportForVectors = false, + protected bool $supportForSpatialAttributes = false, + protected bool $supportForObject = false, + protected mixed $attributeCountCallback = null, + protected mixed $attributeWidthCallback = null, + protected mixed $filterCallback = null, + protected bool $isMigrating = false, + protected bool $sharedTables = false, + ) { + foreach ($attributes as $attribute) { + $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); + $this->attributes[$key] = $attribute; + } + foreach ($schemaAttributes as $attribute) { + $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); + $this->schemaAttributes[$key] = $attribute; + } + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_OBJECT; + } + + /** + * Returns validator description + * @return string + */ + public function getDescription(): string + { + return $this->message; + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Is valid. + * + * Returns true if attribute is valid. + * @param Document $value + * @return bool + * @throws DatabaseException + * @throws DuplicateException + * @throws LimitException + */ + public function isValid($value): bool + { + if (!$this->checkDuplicateId($value)) { + return false; + } + if (!$this->checkDuplicateInSchema($value)) { + return false; + } + if (!$this->checkRequiredFilters($value)) { + return false; + } + if (!$this->checkFormat($value)) { + return false; + } + if (!$this->checkAttributeLimits($value)) { + return false; + } + if (!$this->checkType($value)) { + return false; + } + if (!$this->checkDefaultValue($value)) { + return false; + } + + return true; + } + + /** + * Check for duplicate attribute ID in collection metadata + * + * @param Document $attribute + * @return bool + * @throws DuplicateException + */ + public function checkDuplicateId(Document $attribute): bool + { + $id = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + + foreach ($this->attributes as $existingAttribute) { + if (\strtolower($existingAttribute->getId()) === \strtolower($id)) { + $this->message = 'Attribute already exists in metadata'; + throw new DuplicateException($this->message); + } + } + + return true; + } + + /** + * Check for duplicate attribute ID in schema + * + * @param Document $attribute + * @return bool + * @throws DuplicateException + */ + public function checkDuplicateInSchema(Document $attribute): bool + { + if (!$this->supportForSchemaAttributes) { + return true; + } + + if ($this->sharedTables && $this->isMigrating) { + return true; + } + + $id = $attribute->getAttribute('key', $attribute->getAttribute('$id')); + + foreach ($this->schemaAttributes as $schemaAttribute) { + $schemaId = $this->filterCallback ? ($this->filterCallback)($schemaAttribute->getId()) : $schemaAttribute->getId(); + if (\strtolower($schemaId) === \strtolower($id)) { + $this->message = 'Attribute already exists in schema'; + throw new DuplicateException($this->message); + } + } + + return true; + } + + /** + * Check if required filters are present for the attribute type + * + * @param Document $attribute + * @return bool + * @throws DatabaseException + */ + public function checkRequiredFilters(Document $attribute): bool + { + $type = $attribute->getAttribute('type'); + $filters = $attribute->getAttribute('filters', []); + + $requiredFilters = $this->getRequiredFilters($type); + if (!empty(\array_diff($requiredFilters, $filters))) { + $this->message = "Attribute of type: $type requires the following filters: " . implode(",", $requiredFilters); + throw new DatabaseException($this->message); + } + + return true; + } + + /** + * Get the list of required filters for each data type + * + * @param string|null $type Type of the attribute + * + * @return array + */ + protected function getRequiredFilters(?string $type): array + { + return match ($type) { + Database::VAR_DATETIME => ['datetime'], + default => [], + }; + } + + /** + * Check if format is valid for the attribute type + * + * @param Document $attribute + * @return bool + * @throws DatabaseException + */ + public function checkFormat(Document $attribute): bool + { + $format = $attribute->getAttribute('format'); + $type = $attribute->getAttribute('type'); + + if ($format && !Structure::hasFormat($format, $type)) { + $this->message = 'Format ("' . $format . '") not available for this attribute type ("' . $type . '")'; + throw new DatabaseException($this->message); + } + + return true; + } + + /** + * Check attribute limits (count and width) + * + * @param Document $attribute + * @return bool + * @throws LimitException + */ + public function checkAttributeLimits(Document $attribute): bool + { + if ($this->attributeCountCallback === null || $this->attributeWidthCallback === null) { + return true; + } + + $attributeCount = ($this->attributeCountCallback)($attribute); + $attributeWidth = ($this->attributeWidthCallback)($attribute); + + if ($this->maxAttributes > 0 && $attributeCount > $this->maxAttributes) { + $this->message = 'Column limit reached. Cannot create new attribute. Current attribute count is ' . $attributeCount . ' but the maximum is ' . $this->maxAttributes . '. Remove some attributes to free up space.'; + throw new LimitException($this->message); + } + + if ($this->maxWidth > 0 && $attributeWidth >= $this->maxWidth) { + $this->message = 'Row width limit reached. Cannot create new attribute. Current row width is ' . $attributeWidth . ' bytes but the maximum is ' . $this->maxWidth . ' bytes. Reduce the size of existing attributes or remove some attributes to free up space.'; + throw new LimitException($this->message); + } + + return true; + } + + /** + * Check attribute type and type-specific constraints + * + * @param Document $attribute + * @return bool + * @throws DatabaseException + */ + public function checkType(Document $attribute): bool + { + $type = $attribute->getAttribute('type'); + $size = $attribute->getAttribute('size', 0); + $signed = $attribute->getAttribute('signed', true); + $array = $attribute->getAttribute('array', false); + $default = $attribute->getAttribute('default'); + + switch ($type) { + case Database::VAR_ID: + break; + + case Database::VAR_STRING: + if ($size > $this->maxStringLength) { + $this->message = 'Max size allowed for string is: ' . number_format($this->maxStringLength); + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_VARCHAR: + if ($size > $this->maxVarcharLength) { + $this->message = 'Max size allowed for varchar is: ' . number_format($this->maxVarcharLength); + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_TEXT: + if ($size > 65535) { + $this->message = 'Max size allowed for text is: 65535'; + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_MEDIUMTEXT: + if ($size > 16777215) { + $this->message = 'Max size allowed for mediumtext is: 16777215'; + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_LONGTEXT: + if ($size > 4294967295) { + $this->message = 'Max size allowed for longtext is: 4294967295'; + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_INTEGER: + $limit = ($signed) ? $this->maxIntLength / 2 : $this->maxIntLength; + if ($size > $limit) { + $this->message = 'Max size allowed for int is: ' . number_format($limit); + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_FLOAT: + case Database::VAR_BOOLEAN: + case Database::VAR_DATETIME: + case Database::VAR_RELATIONSHIP: + break; + + case Database::VAR_OBJECT: + if (!$this->supportForObject) { + $this->message = 'Object attributes are not supported'; + throw new DatabaseException($this->message); + } + if (!empty($size)) { + $this->message = 'Size must be empty for object attributes'; + throw new DatabaseException($this->message); + } + if (!empty($array)) { + $this->message = 'Object attributes cannot be arrays'; + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_POINT: + case Database::VAR_LINESTRING: + case Database::VAR_POLYGON: + if (!$this->supportForSpatialAttributes) { + $this->message = 'Spatial attributes are not supported'; + throw new DatabaseException($this->message); + } + if (!empty($size)) { + $this->message = 'Size must be empty for spatial attributes'; + throw new DatabaseException($this->message); + } + if (!empty($array)) { + $this->message = 'Spatial attributes cannot be arrays'; + throw new DatabaseException($this->message); + } + break; + + case Database::VAR_VECTOR: + if (!$this->supportForVectors) { + $this->message = 'Vector types are not supported by the current database'; + throw new DatabaseException($this->message); + } + if ($array) { + $this->message = 'Vector type cannot be an array'; + throw new DatabaseException($this->message); + } + if ($size <= 0) { + $this->message = 'Vector dimensions must be a positive integer'; + throw new DatabaseException($this->message); + } + if ($size > Database::MAX_VECTOR_DIMENSIONS) { + $this->message = 'Vector dimensions cannot exceed ' . Database::MAX_VECTOR_DIMENSIONS; + throw new DatabaseException($this->message); + } + + // Validate default value if provided + if ($default !== null) { + if (!is_array($default)) { + $this->message = 'Vector default value must be an array'; + throw new DatabaseException($this->message); + } + if (count($default) !== $size) { + $this->message = 'Vector default value must have exactly ' . $size . ' elements'; + throw new DatabaseException($this->message); + } + foreach ($default as $component) { + if (!is_numeric($component)) { + $this->message = 'Vector default value must contain only numeric elements'; + throw new DatabaseException($this->message); + } + } + } + break; + + default: + $supportedTypes = [ + 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 + ]; + if ($this->supportForVectors) { + $supportedTypes[] = Database::VAR_VECTOR; + } + if ($this->supportForSpatialAttributes) { + \array_push($supportedTypes, ...Database::SPATIAL_TYPES); + } + if ($this->supportForObject) { + $supportedTypes[] = Database::VAR_OBJECT; + } + $this->message = 'Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes); + throw new DatabaseException($this->message); + } + + return true; + } + + /** + * Check default value constraints and type matching + * + * @param Document $attribute + * @return bool + * @throws DatabaseException + */ + public function checkDefaultValue(Document $attribute): bool + { + $default = $attribute->getAttribute('default'); + $required = $attribute->getAttribute('required', false); + $type = $attribute->getAttribute('type'); + $array = $attribute->getAttribute('array', false); + + if (\is_null($default)) { + return true; + } + + if ($required === true) { + $this->message = 'Cannot set a default value for a required attribute'; + throw new DatabaseException($this->message); + } + + // Reject array defaults for non-array attributes (except vectors, spatial types, and objects which use arrays internally) + if (\is_array($default) && !$array && !\in_array($type, [Database::VAR_VECTOR, Database::VAR_OBJECT, ...Database::SPATIAL_TYPES], true)) { + $this->message = 'Cannot set an array default value for a non-array attribute'; + throw new DatabaseException($this->message); + } + + $this->validateDefaultTypes($type, $default); + + return true; + } + + /** + * Function to validate if the default value of an attribute matches its attribute type + * + * @param string $type Type of the attribute + * @param mixed $default Default value of the attribute + * + * @return void + * @throws DatabaseException + */ + protected function validateDefaultTypes(string $type, mixed $default): void + { + $defaultType = \gettype($default); + + if ($defaultType === 'NULL') { + // Disable null. No validation required + return; + } + + if ($defaultType === 'array') { + // Spatial types require the array itself + if (!in_array($type, Database::SPATIAL_TYPES) && $type != Database::VAR_OBJECT) { + foreach ($default as $value) { + $this->validateDefaultTypes($type, $value); + } + } + return; + } + + switch ($type) { + case Database::VAR_STRING: + case Database::VAR_VARCHAR: + case Database::VAR_TEXT: + case Database::VAR_MEDIUMTEXT: + case Database::VAR_LONGTEXT: + if ($defaultType !== 'string') { + $this->message = 'Default value ' . $default . ' does not match given type ' . $type; + throw new DatabaseException($this->message); + } + break; + case Database::VAR_INTEGER: + case Database::VAR_FLOAT: + case Database::VAR_BOOLEAN: + if ($type !== $defaultType) { + $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; + throw new DatabaseException($this->message); + } + break; + case Database::VAR_VECTOR: + // When validating individual vector components (from recursion), they should be numeric + if ($defaultType !== 'double' && $defaultType !== 'integer') { + $this->message = 'Vector components must be numeric values (float or integer)'; + throw new DatabaseException($this->message); + } + break; + default: + $supportedTypes = [ + 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 + ]; + if ($this->supportForVectors) { + $supportedTypes[] = Database::VAR_VECTOR; + } + if ($this->supportForSpatialAttributes) { + \array_push($supportedTypes, ...Database::SPATIAL_TYPES); + } + $this->message = 'Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes); + throw new DatabaseException($this->message); + } + } +} diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index e2fc70a0b..90435c4f3 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -290,7 +290,15 @@ public function checkFulltextIndexNonString(Document $index): bool if ($index->getAttribute('type') === Database::INDEX_FULLTEXT) { foreach ($index->getAttribute('attributes', []) as $attribute) { $attribute = $this->attributes[\strtolower($attribute)] ?? new Document(); - if ($attribute->getAttribute('type', '') !== Database::VAR_STRING) { + $attributeType = $attribute->getAttribute('type', ''); + $validFulltextTypes = [ + Database::VAR_STRING, + Database::VAR_VARCHAR, + Database::VAR_TEXT, + Database::VAR_MEDIUMTEXT, + Database::VAR_LONGTEXT + ]; + if (!in_array($attributeType, $validFulltextTypes)) { $this->message = 'Attribute "' . $attribute->getAttribute('key', $attribute->getAttribute('$id')) . '" cannot be part of a fulltext index, must be of type string'; return false; } @@ -344,7 +352,13 @@ public function checkArrayIndexes(Document $index): bool $this->message = 'Indexing an array attribute is not supported'; return false; } - } elseif ($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + } elseif (!in_array($attribute->getAttribute('type'), [ + Database::VAR_STRING, + Database::VAR_VARCHAR, + Database::VAR_TEXT, + Database::VAR_MEDIUMTEXT, + Database::VAR_LONGTEXT + ]) && !empty($lengths[$attributePosition])) { $this->message = 'Cannot set a length on "' . $attribute->getAttribute('type') . '" attributes'; return false; } @@ -378,6 +392,10 @@ public function checkIndexLengths(Document $index): bool switch ($attribute->getAttribute('type')) { case Database::VAR_STRING: + case Database::VAR_VARCHAR: + case Database::VAR_TEXT: + case Database::VAR_MEDIUMTEXT: + case Database::VAR_LONGTEXT: $attributeSize = $attribute->getAttribute('size', 0); $indexLength = !empty($lengths[$attributePosition]) ? $lengths[$attributePosition] : $attributeSize; break; @@ -576,9 +594,17 @@ public function checkTrigramIndexes(Document $index): bool $attributes = $index->getAttribute('attributes', []); + $validStringTypes = [ + Database::VAR_STRING, + Database::VAR_VARCHAR, + Database::VAR_TEXT, + Database::VAR_MEDIUMTEXT, + Database::VAR_LONGTEXT + ]; + foreach ($attributes as $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); - if ($attribute->getAttribute('type', '') !== Database::VAR_STRING) { + if (!in_array($attribute->getAttribute('type', ''), $validStringTypes)) { $this->message = 'Trigram index can only be created on string type attributes'; return false; } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index d68ccbf8b..6c50b9315 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -142,6 +142,10 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s break; case Database::VAR_STRING: + case Database::VAR_VARCHAR: + case Database::VAR_TEXT: + case Database::VAR_MEDIUMTEXT: + case Database::VAR_LONGTEXT: $validator = new Text(0, 0); break; diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index ac1739089..417e10c27 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -343,6 +343,10 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) $validators[] = new Sequence($this->idAttributeType, $attribute['$id'] === '$sequence'); break; + case Database::VAR_VARCHAR: + case Database::VAR_TEXT: + case Database::VAR_MEDIUMTEXT: + case Database::VAR_LONGTEXT: case Database::VAR_STRING: $validators[] = new Text($size, min: 0); break; diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php index 9f1a4b31f..ce6c0f30b 100644 --- a/tests/e2e/Adapter/Scopes/AttributeTests.php +++ b/tests/e2e/Adapter/Scopes/AttributeTests.php @@ -52,6 +52,18 @@ public function invalidDefaultValues(): array [Database::VAR_BOOLEAN, 0], [Database::VAR_BOOLEAN, "false"], [Database::VAR_BOOLEAN, 0.5], + [Database::VAR_VARCHAR, 1], + [Database::VAR_VARCHAR, 1.5], + [Database::VAR_VARCHAR, false], + [Database::VAR_TEXT, 1], + [Database::VAR_TEXT, 1.5], + [Database::VAR_TEXT, true], + [Database::VAR_MEDIUMTEXT, 1], + [Database::VAR_MEDIUMTEXT, 1.5], + [Database::VAR_MEDIUMTEXT, false], + [Database::VAR_LONGTEXT, 1], + [Database::VAR_LONGTEXT, 1.5], + [Database::VAR_LONGTEXT, true], ]; } @@ -72,23 +84,37 @@ public function testCreateDeleteAttribute(): void $this->assertEquals(true, $database->createAttribute('attributes', 'boolean', Database::VAR_BOOLEAN, 0, true)); $this->assertEquals(true, $database->createAttribute('attributes', 'id', Database::VAR_ID, 0, true)); + // New string types + $this->assertEquals(true, $database->createAttribute('attributes', 'varchar1', Database::VAR_VARCHAR, 255, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'varchar2', Database::VAR_VARCHAR, 128, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'text1', Database::VAR_TEXT, 65535, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'mediumtext1', Database::VAR_MEDIUMTEXT, 16777215, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'longtext1', Database::VAR_LONGTEXT, 4294967295, true)); + $this->assertEquals(true, $database->createIndex('attributes', 'id_index', Database::INDEX_KEY, ['id'])); $this->assertEquals(true, $database->createIndex('attributes', 'string1_index', Database::INDEX_KEY, ['string1'])); $this->assertEquals(true, $database->createIndex('attributes', 'string2_index', Database::INDEX_KEY, ['string2'], [255])); $this->assertEquals(true, $database->createIndex('attributes', 'multi_index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128])); + $this->assertEquals(true, $database->createIndex('attributes', 'varchar1_index', Database::INDEX_KEY, ['varchar1'])); + $this->assertEquals(true, $database->createIndex('attributes', 'varchar2_index', Database::INDEX_KEY, ['varchar2'])); + $this->assertEquals(true, $database->createIndex('attributes', 'text1_index', Database::INDEX_KEY, ['text1'], [255])); $collection = $database->getCollection('attributes'); - $this->assertCount(9, $collection->getAttribute('attributes')); - $this->assertCount(4, $collection->getAttribute('indexes')); + $this->assertCount(14, $collection->getAttribute('attributes')); + $this->assertCount(7, $collection->getAttribute('indexes')); // Array $this->assertEquals(true, $database->createAttribute('attributes', 'string_list', Database::VAR_STRING, 128, true, null, true, true)); $this->assertEquals(true, $database->createAttribute('attributes', 'integer_list', Database::VAR_INTEGER, 0, true, null, true, true)); $this->assertEquals(true, $database->createAttribute('attributes', 'float_list', Database::VAR_FLOAT, 0, true, null, true, true)); $this->assertEquals(true, $database->createAttribute('attributes', 'boolean_list', Database::VAR_BOOLEAN, 0, true, null, true, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'varchar_list', Database::VAR_VARCHAR, 128, true, null, true, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'text_list', Database::VAR_TEXT, 65535, true, null, true, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'mediumtext_list', Database::VAR_MEDIUMTEXT, 16777215, true, null, true, true)); + $this->assertEquals(true, $database->createAttribute('attributes', 'longtext_list', Database::VAR_LONGTEXT, 4294967295, true, null, true, true)); $collection = $database->getCollection('attributes'); - $this->assertCount(13, $collection->getAttribute('attributes')); + $this->assertCount(22, $collection->getAttribute('attributes')); // Default values $this->assertEquals(true, $database->createAttribute('attributes', 'string_default', Database::VAR_STRING, 256, false, 'test')); @@ -96,9 +122,13 @@ public function testCreateDeleteAttribute(): void $this->assertEquals(true, $database->createAttribute('attributes', 'float_default', Database::VAR_FLOAT, 0, false, 1.5)); $this->assertEquals(true, $database->createAttribute('attributes', 'boolean_default', Database::VAR_BOOLEAN, 0, false, false)); $this->assertEquals(true, $database->createAttribute('attributes', 'datetime_default', Database::VAR_DATETIME, 0, false, '2000-06-12T14:12:55.000+00:00', true, false, null, [], ['datetime'])); + $this->assertEquals(true, $database->createAttribute('attributes', 'varchar_default', Database::VAR_VARCHAR, 255, false, 'varchar default')); + $this->assertEquals(true, $database->createAttribute('attributes', 'text_default', Database::VAR_TEXT, 65535, false, 'text default')); + $this->assertEquals(true, $database->createAttribute('attributes', 'mediumtext_default', Database::VAR_MEDIUMTEXT, 16777215, false, 'mediumtext default')); + $this->assertEquals(true, $database->createAttribute('attributes', 'longtext_default', Database::VAR_LONGTEXT, 4294967295, false, 'longtext default')); $collection = $database->getCollection('attributes'); - $this->assertCount(18, $collection->getAttribute('attributes')); + $this->assertCount(31, $collection->getAttribute('attributes')); // Delete $this->assertEquals(true, $database->deleteAttribute('attributes', 'string1')); @@ -110,9 +140,14 @@ public function testCreateDeleteAttribute(): void $this->assertEquals(true, $database->deleteAttribute('attributes', 'float')); $this->assertEquals(true, $database->deleteAttribute('attributes', 'boolean')); $this->assertEquals(true, $database->deleteAttribute('attributes', 'id')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'varchar1')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'varchar2')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'text1')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'mediumtext1')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'longtext1')); $collection = $database->getCollection('attributes'); - $this->assertCount(9, $collection->getAttribute('attributes')); + $this->assertCount(17, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); // Delete Array @@ -120,9 +155,13 @@ public function testCreateDeleteAttribute(): void $this->assertEquals(true, $database->deleteAttribute('attributes', 'integer_list')); $this->assertEquals(true, $database->deleteAttribute('attributes', 'float_list')); $this->assertEquals(true, $database->deleteAttribute('attributes', 'boolean_list')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'varchar_list')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'text_list')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'mediumtext_list')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'longtext_list')); $collection = $database->getCollection('attributes'); - $this->assertCount(5, $collection->getAttribute('attributes')); + $this->assertCount(9, $collection->getAttribute('attributes')); // Delete default $this->assertEquals(true, $database->deleteAttribute('attributes', 'string_default')); @@ -130,6 +169,10 @@ public function testCreateDeleteAttribute(): void $this->assertEquals(true, $database->deleteAttribute('attributes', 'float_default')); $this->assertEquals(true, $database->deleteAttribute('attributes', 'boolean_default')); $this->assertEquals(true, $database->deleteAttribute('attributes', 'datetime_default')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'varchar_default')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'text_default')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'mediumtext_default')); + $this->assertEquals(true, $database->deleteAttribute('attributes', 'longtext_default')); $collection = $database->getCollection('attributes'); $this->assertCount(0, $collection->getAttribute('attributes')); @@ -2195,4 +2238,138 @@ public function testCreateAttributesDelete(): void $this->assertCount(1, $attrs); $this->assertEquals('b', $attrs[0]['$id']); } + + /** + * @depends testCreateDeleteAttribute + */ + public function testStringTypeAttributes(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $database->createCollection('stringTypes'); + + // Create attributes with different string types + $this->assertEquals(true, $database->createAttribute('stringTypes', 'varchar_field', Database::VAR_VARCHAR, 255, false, 'default varchar')); + $this->assertEquals(true, $database->createAttribute('stringTypes', 'text_field', Database::VAR_TEXT, 65535, false)); + $this->assertEquals(true, $database->createAttribute('stringTypes', 'mediumtext_field', Database::VAR_MEDIUMTEXT, 16777215, false)); + $this->assertEquals(true, $database->createAttribute('stringTypes', 'longtext_field', Database::VAR_LONGTEXT, 4294967295, false)); + + // Test with array types + $this->assertEquals(true, $database->createAttribute('stringTypes', 'varchar_array', Database::VAR_VARCHAR, 128, false, null, true, true)); + $this->assertEquals(true, $database->createAttribute('stringTypes', 'text_array', Database::VAR_TEXT, 65535, false, null, true, true)); + + $collection = $database->getCollection('stringTypes'); + $this->assertCount(6, $collection->getAttribute('attributes')); + + // Test VARCHAR with valid data + $doc1 = $database->createDocument('stringTypes', new Document([ + '$id' => ID::custom('doc1'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'varchar_field' => 'This is a varchar field with 255 max length', + 'text_field' => \str_repeat('a', 1000), + 'mediumtext_field' => \str_repeat('b', 100000), + 'longtext_field' => \str_repeat('c', 1000000), + ])); + + $this->assertEquals('This is a varchar field with 255 max length', $doc1->getAttribute('varchar_field')); + $this->assertEquals(\str_repeat('a', 1000), $doc1->getAttribute('text_field')); + $this->assertEquals(\str_repeat('b', 100000), $doc1->getAttribute('mediumtext_field')); + $this->assertEquals(\str_repeat('c', 1000000), $doc1->getAttribute('longtext_field')); + + // Test VARCHAR with default value + $doc2 = $database->createDocument('stringTypes', new Document([ + '$id' => ID::custom('doc2'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ])); + + $this->assertEquals('default varchar', $doc2->getAttribute('varchar_field')); + $this->assertNull($doc2->getAttribute('text_field')); + + // Test array types + $doc3 = $database->createDocument('stringTypes', new Document([ + '$id' => ID::custom('doc3'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'varchar_array' => ['test1', 'test2', 'test3'], + 'text_array' => [\str_repeat('x', 1000), \str_repeat('y', 2000)], + ])); + + $this->assertEquals(['test1', 'test2', 'test3'], $doc3->getAttribute('varchar_array')); + $this->assertEquals([\str_repeat('x', 1000), \str_repeat('y', 2000)], $doc3->getAttribute('text_array')); + + // Test VARCHAR size constraint (should fail) - only for adapters that support attributes + if ($database->getAdapter()->getSupportForAttributes()) { + try { + $database->createDocument('stringTypes', new Document([ + '$id' => ID::custom('doc4'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'varchar_field' => \str_repeat('a', 256), // Too long for VARCHAR(255) + ])); + $this->fail('Failed to throw exception for VARCHAR size violation'); + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + // Test TEXT size constraint (should fail) + try { + $database->createDocument('stringTypes', new Document([ + '$id' => ID::custom('doc5'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'text_field' => \str_repeat('a', 65536), // Too long for TEXT(65535) + ])); + $this->fail('Failed to throw exception for TEXT size violation'); + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + } + + // Test querying by VARCHAR field + $this->assertEquals(true, $database->createIndex('stringTypes', 'varchar_index', Database::INDEX_KEY, ['varchar_field'])); + + $results = $database->find('stringTypes', [ + Query::equal('varchar_field', ['This is a varchar field with 255 max length']) + ]); + $this->assertCount(1, $results); + $this->assertEquals('doc1', $results[0]->getId()); + + // Test updating VARCHAR field + $database->updateDocument('stringTypes', 'doc1', new Document([ + '$id' => 'doc1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'varchar_field' => 'Updated varchar value', + ])); + + $updatedDoc = $database->getDocument('stringTypes', 'doc1'); + $this->assertEquals('Updated varchar value', $updatedDoc->getAttribute('varchar_field')); + } } diff --git a/tests/unit/Validator/AttributeTest.php b/tests/unit/Validator/AttributeTest.php new file mode 100644 index 000000000..2f7303cd1 --- /dev/null +++ b/tests/unit/Validator/AttributeTest.php @@ -0,0 +1,1749 @@ + ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]) + ], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DuplicateException::class); + $this->expectExceptionMessage('Attribute already exists in metadata'); + $validator->isValid($attribute); + } + + public function testValidStringAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testStringSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 1000, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 2000, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for string is: 1,000'); + $validator->isValid($attribute); + } + + public function testVarcharSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 1000, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_VARCHAR, + 'size' => 2000, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for varchar is: 1,000'); + $validator->isValid($attribute); + } + + public function testTextSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_TEXT, + 'size' => 70000, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for text is: 65535'); + $validator->isValid($attribute); + } + + public function testMediumtextSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_MEDIUMTEXT, + 'size' => 20000000, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for mediumtext is: 16777215'); + $validator->isValid($attribute); + } + + public function testIntegerSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: 100, + ); + + $attribute = new Document([ + '$id' => ID::custom('count'), + 'key' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 200, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for int is: 50'); + $validator->isValid($attribute); + } + + public function testUnknownType(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('test'), + 'key' => 'test', + 'type' => 'unknown_type', + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessageMatches('/Unknown attribute type: unknown_type/'); + $validator->isValid($attribute); + } + + public function testRequiredFiltersForDatetime(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('created'), + 'key' => 'created', + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => false, + 'array' => false, + 'filters' => [], // Missing datetime filter + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Attribute of type: datetime requires the following filters: datetime'); + $validator->isValid($attribute); + } + + public function testValidDatetimeWithFilter(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('created'), + 'key' => 'created', + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => false, + 'array' => false, + 'filters' => ['datetime'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testDefaultValueOnRequiredAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => true, + 'default' => 'default value', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Cannot set a default value for a required attribute'); + $validator->isValid($attribute); + } + + public function testDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('count'), + 'key' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 4, + 'required' => false, + 'default' => 'not_an_integer', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value not_an_integer does not match given type integer'); + $validator->isValid($attribute); + } + + public function testVectorNotSupported(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: false, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 128, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector types are not supported by the current database'); + $validator->isValid($attribute); + } + + public function testVectorCannotBeArray(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embeddings'), + 'key' => 'embeddings', + 'type' => Database::VAR_VECTOR, + 'size' => 128, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => true, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector type cannot be an array'); + $validator->isValid($attribute); + } + + public function testVectorInvalidDimensions(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector dimensions must be a positive integer'); + $validator->isValid($attribute); + } + + public function testVectorDimensionsExceedsMax(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 20000, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector dimensions cannot exceed 16000'); + $validator->isValid($attribute); + } + + public function testSpatialNotSupported(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSpatialAttributes: false, + ); + + $attribute = new Document([ + '$id' => ID::custom('location'), + 'key' => 'location', + 'type' => Database::VAR_POINT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['point'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Spatial attributes are not supported'); + $validator->isValid($attribute); + } + + public function testSpatialCannotBeArray(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSpatialAttributes: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('locations'), + 'key' => 'locations', + 'type' => Database::VAR_POINT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => true, + 'filters' => ['point'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Spatial attributes cannot be arrays'); + $validator->isValid($attribute); + } + + public function testSpatialMustHaveEmptySize(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSpatialAttributes: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('location'), + 'key' => 'location', + 'type' => Database::VAR_POINT, + 'size' => 100, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['point'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Size must be empty for spatial attributes'); + $validator->isValid($attribute); + } + + public function testObjectNotSupported(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForObject: false, + ); + + $attribute = new Document([ + '$id' => ID::custom('metadata'), + 'key' => 'metadata', + 'type' => Database::VAR_OBJECT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['object'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Object attributes are not supported'); + $validator->isValid($attribute); + } + + public function testObjectCannotBeArray(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForObject: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('metadata'), + 'key' => 'metadata', + 'type' => Database::VAR_OBJECT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => true, + 'filters' => ['object'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Object attributes cannot be arrays'); + $validator->isValid($attribute); + } + + public function testObjectMustHaveEmptySize(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForObject: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('metadata'), + 'key' => 'metadata', + 'type' => Database::VAR_OBJECT, + 'size' => 100, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['object'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Size must be empty for object attributes'); + $validator->isValid($attribute); + } + + public function testAttributeLimitExceeded(): void + { + $validator = new Attribute( + attributes: [], + maxAttributes: 5, + maxWidth: 0, + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + attributeCountCallback: fn () => 10, + attributeWidthCallback: fn () => 100, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(LimitException::class); + $this->expectExceptionMessage('Column limit reached'); + $validator->isValid($attribute); + } + + public function testRowWidthLimitExceeded(): void + { + $validator = new Attribute( + attributes: [], + maxAttributes: 100, + maxWidth: 1000, + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + attributeCountCallback: fn () => 5, + attributeWidthCallback: fn () => 1500, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(LimitException::class); + $this->expectExceptionMessage('Row width limit reached'); + $validator->isValid($attribute); + } + + public function testVectorDefaultValueNotArray(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 3, + 'required' => false, + 'default' => 'not_an_array', + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector default value must be an array'); + $validator->isValid($attribute); + } + + public function testVectorDefaultValueWrongElementCount(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 3, + 'required' => false, + 'default' => [1.0, 2.0], + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector default value must have exactly 3 elements'); + $validator->isValid($attribute); + } + + public function testVectorDefaultValueNonNumericElements(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 3, + 'required' => false, + 'default' => [1.0, 'not_a_number', 3.0], + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Vector default value must contain only numeric elements'); + $validator->isValid($attribute); + } + + public function testLongtextSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_LONGTEXT, + 'size' => 5000000000, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for longtext is: 4294967295'); + $validator->isValid($attribute); + } + + public function testValidVarcharAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('name'), + 'key' => 'name', + 'type' => Database::VAR_VARCHAR, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidTextAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_TEXT, + 'size' => 65535, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidMediumtextAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_MEDIUMTEXT, + 'size' => 16777215, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidLongtextAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_LONGTEXT, + 'size' => 4294967295, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidFloatAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('price'), + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidBooleanAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('active'), + 'key' => 'active', + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testFloatDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('price'), + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 0, + 'required' => false, + 'default' => 'not_a_float', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value not_a_float does not match given type double'); + $validator->isValid($attribute); + } + + public function testBooleanDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('active'), + 'key' => 'active', + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => false, + 'default' => 'not_a_boolean', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value not_a_boolean does not match given type boolean'); + $validator->isValid($attribute); + } + + public function testStringDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => 123, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 123 does not match given type string'); + $validator->isValid($attribute); + } + + public function testValidStringWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => 'default title', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidIntegerWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('count'), + 'key' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 4, + 'required' => false, + 'default' => 42, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidFloatWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('price'), + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 0, + 'required' => false, + 'default' => 19.99, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidBooleanWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('active'), + 'key' => 'active', + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => false, + 'default' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testUnsignedIntegerSizeLimit(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: 100, + ); + + // Unsigned allows double the size + $attribute = new Document([ + '$id' => ID::custom('count'), + 'key' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 80, + 'required' => false, + 'default' => null, + 'signed' => false, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testUnsignedIntegerSizeTooLarge(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: 100, + ); + + $attribute = new Document([ + '$id' => ID::custom('count'), + 'key' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 150, + 'required' => false, + 'default' => null, + 'signed' => false, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Max size allowed for int is: 100'); + $validator->isValid($attribute); + } + + public function testDuplicateAttributeIdCaseInsensitive(): void + { + $validator = new Attribute( + attributes: [ + new Document([ + '$id' => ID::custom('Title'), + 'key' => 'Title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]) + ], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DuplicateException::class); + $this->expectExceptionMessage('Attribute already exists in metadata'); + $validator->isValid($attribute); + } + + public function testDuplicateInSchema(): void + { + $validator = new Attribute( + attributes: [], + schemaAttributes: [ + new Document([ + '$id' => ID::custom('existing_column'), + 'key' => 'existing_column', + 'type' => Database::VAR_STRING, + 'size' => 255, + ]) + ], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSchemaAttributes: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('existing_column'), + 'key' => 'existing_column', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DuplicateException::class); + $this->expectExceptionMessage('Attribute already exists in schema'); + $validator->isValid($attribute); + } + + public function testSchemaCheckSkippedWhenMigrating(): void + { + $validator = new Attribute( + attributes: [], + schemaAttributes: [ + new Document([ + '$id' => ID::custom('existing_column'), + 'key' => 'existing_column', + 'type' => Database::VAR_STRING, + 'size' => 255, + ]) + ], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSchemaAttributes: true, + isMigrating: true, + sharedTables: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('existing_column'), + 'key' => 'existing_column', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidLinestringAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSpatialAttributes: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('route'), + 'key' => 'route', + 'type' => Database::VAR_LINESTRING, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['linestring'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidPolygonAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSpatialAttributes: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('area'), + 'key' => 'area', + 'type' => Database::VAR_POLYGON, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['polygon'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidPointAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForSpatialAttributes: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('location'), + 'key' => 'location', + 'type' => Database::VAR_POINT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['point'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidVectorAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 128, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidVectorWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForVectors: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('embedding'), + 'key' => 'embedding', + 'type' => Database::VAR_VECTOR, + 'size' => 3, + 'required' => false, + 'default' => [1.0, 2.0, 3.0], + 'signed' => true, + 'array' => false, + 'filters' => ['vector'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidObjectAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + supportForObject: true, + ); + + $attribute = new Document([ + '$id' => ID::custom('metadata'), + 'key' => 'metadata', + 'type' => Database::VAR_OBJECT, + 'size' => 0, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => ['object'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testArrayStringAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('tags'), + 'key' => 'tags', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => true, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testArrayWithDefaultValues(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('tags'), + 'key' => 'tags', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => ['tag1', 'tag2', 'tag3'], + 'signed' => true, + 'array' => true, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testArrayDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('tags'), + 'key' => 'tags', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => ['tag1', 123, 'tag3'], + 'signed' => true, + 'array' => true, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 123 does not match given type string'); + $validator->isValid($attribute); + } + + public function testDatetimeDefaultValueMustBeString(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('created'), + 'key' => 'created', + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'required' => false, + 'default' => 12345, + 'signed' => false, + 'array' => false, + 'filters' => ['datetime'], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 12345 does not match given type datetime'); + $validator->isValid($attribute); + } + + public function testValidDatetimeWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('created'), + 'key' => 'created', + 'type' => Database::VAR_DATETIME, + 'size' => 0, + 'required' => false, + 'default' => '2024-01-01T00:00:00.000Z', + 'signed' => false, + 'array' => false, + 'filters' => ['datetime'], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testVarcharDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('name'), + 'key' => 'name', + 'type' => Database::VAR_VARCHAR, + 'size' => 255, + 'required' => false, + 'default' => 123, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 123 does not match given type varchar'); + $validator->isValid($attribute); + } + + public function testTextDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_TEXT, + 'size' => 65535, + 'required' => false, + 'default' => 123, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 123 does not match given type text'); + $validator->isValid($attribute); + } + + public function testMediumtextDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_MEDIUMTEXT, + 'size' => 16777215, + 'required' => false, + 'default' => 123, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 123 does not match given type mediumtext'); + $validator->isValid($attribute); + } + + public function testLongtextDefaultValueTypeMismatch(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_LONGTEXT, + 'size' => 4294967295, + 'required' => false, + 'default' => 123, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Default value 123 does not match given type longtext'); + $validator->isValid($attribute); + } + + public function testValidVarcharWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('name'), + 'key' => 'name', + 'type' => Database::VAR_VARCHAR, + 'size' => 255, + 'required' => false, + 'default' => 'default name', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidTextWithDefaultValue(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('content'), + 'key' => 'content', + 'type' => Database::VAR_TEXT, + 'size' => 65535, + 'required' => false, + 'default' => 'default content', + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testValidIntegerAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('count'), + 'key' => 'count', + 'type' => Database::VAR_INTEGER, + 'size' => 4, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testNullDefaultValueAllowed(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->assertTrue($validator->isValid($attribute)); + } + + public function testArrayDefaultOnNonArrayAttribute(): void + { + $validator = new Attribute( + attributes: [], + maxStringLength: 16777216, + maxVarcharLength: 65535, + maxIntLength: PHP_INT_MAX, + ); + + $attribute = new Document([ + '$id' => ID::custom('title'), + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => false, + 'default' => ['not', 'allowed'], + 'signed' => true, + 'array' => false, + 'filters' => [], + ]); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Cannot set an array default value for a non-array attribute'); + $validator->isValid($attribute); + } +} diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index 7176527ae..ffc2b62ee 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -101,6 +101,46 @@ class StructureTest extends TestCase 'array' => false, 'filters' => [], ], + [ + '$id' => 'varchar_field', + 'type' => Database::VAR_VARCHAR, + 'format' => '', + 'size' => 255, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'text_field', + 'type' => Database::VAR_TEXT, + 'format' => '', + 'size' => 65535, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'mediumtext_field', + 'type' => Database::VAR_MEDIUMTEXT, + 'format' => '', + 'size' => 16777215, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'longtext_field', + 'type' => Database::VAR_LONGTEXT, + 'format' => '', + 'size' => 4294967295, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [], ]; @@ -849,4 +889,250 @@ public function testMissingRequiredFieldWithoutOperator(): void $this->assertEquals('Invalid document structure: Missing required attribute "rating"', $validator->getDescription()); } + public function testVarcharValidation(): void + { + $validator = new Structure( + new Document($this->collection), + Database::VAR_INTEGER + ); + + $this->assertEquals(true, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'varchar_field' => 'Short varchar text', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'varchar_field' => 123, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals('Invalid document structure: Attribute "varchar_field" has invalid type. Value must be a valid string and no longer than 255 chars', $validator->getDescription()); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'varchar_field' => \str_repeat('a', 256), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals('Invalid document structure: Attribute "varchar_field" has invalid type. Value must be a valid string and no longer than 255 chars', $validator->getDescription()); + } + + public function testTextValidation(): void + { + $validator = new Structure( + new Document($this->collection), + Database::VAR_INTEGER + ); + + $this->assertEquals(true, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'text_field' => \str_repeat('a', 65535), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'text_field' => 123, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals('Invalid document structure: Attribute "text_field" has invalid type. Value must be a valid string and no longer than 65535 chars', $validator->getDescription()); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'text_field' => \str_repeat('a', 65536), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals('Invalid document structure: Attribute "text_field" has invalid type. Value must be a valid string and no longer than 65535 chars', $validator->getDescription()); + } + + public function testMediumtextValidation(): void + { + $validator = new Structure( + new Document($this->collection), + Database::VAR_INTEGER + ); + + $this->assertEquals(true, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'mediumtext_field' => \str_repeat('a', 100000), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'mediumtext_field' => 123, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals('Invalid document structure: Attribute "mediumtext_field" has invalid type. Value must be a valid string and no longer than 16777215 chars', $validator->getDescription()); + } + + public function testLongtextValidation(): void + { + $validator = new Structure( + new Document($this->collection), + Database::VAR_INTEGER + ); + + $this->assertEquals(true, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'longtext_field' => \str_repeat('a', 1000000), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'longtext_field' => 123, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals('Invalid document structure: Attribute "longtext_field" has invalid type. Value must be a valid string and no longer than 4294967295 chars', $validator->getDescription()); + } + + public function testStringTypeArrayValidation(): void + { + $collection = [ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'collections', + 'attributes' => [ + [ + '$id' => 'varchar_array', + 'type' => Database::VAR_VARCHAR, + 'format' => '', + 'size' => 128, + 'required' => false, + 'signed' => true, + 'array' => true, + 'filters' => [], + ], + [ + '$id' => 'text_array', + 'type' => Database::VAR_TEXT, + 'format' => '', + 'size' => 65535, + 'required' => false, + 'signed' => true, + 'array' => true, + 'filters' => [], + ], + ], + 'indexes' => [], + ]; + + $validator = new Structure( + new Document($collection), + Database::VAR_INTEGER + ); + + $this->assertEquals(true, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'varchar_array' => ['test1', 'test2', 'test3'], + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'varchar_array' => [123, 'test2', 'test3'], + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $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()); + + $this->assertEquals(false, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'varchar_array' => [\str_repeat('a', 129), 'test2'], + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + + $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()); + } + }