Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
17 changes: 16 additions & 1 deletion src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1678,6 +1678,21 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool

return "VARCHAR({$size})";

case Database::VAR_VARCHAR:
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})";

Comment thread
abnegate marked this conversation as resolved.
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';

Expand All @@ -1701,7 +1716,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);
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
*
Expand Down
5 changes: 5 additions & 0 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
10 changes: 9 additions & 1 deletion src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
176 changes: 42 additions & 134 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -2232,36 +2238,6 @@ private function validateAttribute(
array $formatOptions,
array $filters
): Document {
// Attribute IDs are case-insensitive
$attributes = $collection->getAttribute('attributes', []);

/** @var array<Document> $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,
Expand All @@ -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);

Comment thread
abnegate marked this conversation as resolved.
return $attribute;
}
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down
Loading