Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 19 additions & 1 deletion src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
}
}

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);

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