Skip to content

Commit b5c16ca

Browse files
authored
Merge pull request #788 from utopia-php/feat-string-types
2 parents 78f7c97 + becf445 commit b5c16ca

File tree

14 files changed

+2926
-145
lines changed

14 files changed

+2926
-145
lines changed

src/Database/Adapter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,13 @@ abstract public function getLimitForIndexes(): int;
899899
*/
900900
abstract public function getMaxIndexLength(): int;
901901

902+
/**
903+
* Get the maximum VARCHAR length for this adapter
904+
*
905+
* @return int
906+
*/
907+
abstract public function getMaxVarcharLength(): int;
908+
902909
/**
903910
* Get the maximum UID length for this adapter
904911
*

src/Database/Adapter/MariaDB.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,24 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
16781678

16791679
return "VARCHAR({$size})";
16801680

1681+
case Database::VAR_VARCHAR:
1682+
if ($size <= 0) {
1683+
throw new DatabaseException('VARCHAR size ' . $size . ' is invalid; must be > 0. Use TEXT, MEDIUMTEXT, or LONGTEXT instead.');
1684+
}
1685+
if ($size > $this->getMaxVarcharLength()) {
1686+
throw new DatabaseException('VARCHAR size ' . $size . ' exceeds maximum varchar length ' . $this->getMaxVarcharLength() . '. Use TEXT, MEDIUMTEXT, or LONGTEXT instead.');
1687+
}
1688+
return "VARCHAR({$size})";
1689+
1690+
case Database::VAR_TEXT:
1691+
return 'TEXT';
1692+
1693+
case Database::VAR_MEDIUMTEXT:
1694+
return 'MEDIUMTEXT';
1695+
1696+
case Database::VAR_LONGTEXT:
1697+
return 'LONGTEXT';
1698+
16811699
case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554
16821700
$signed = ($signed) ? '' : ' UNSIGNED';
16831701

@@ -1701,7 +1719,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
17011719
return 'DATETIME(3)';
17021720

17031721
default:
1704-
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);
1722+
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);
17051723
}
17061724
}
17071725

src/Database/Adapter/Mongo.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,6 +2067,10 @@ private function getMongoTypeCode(string $appwriteType): string
20672067
{
20682068
return match ($appwriteType) {
20692069
Database::VAR_STRING => 'string',
2070+
Database::VAR_VARCHAR => 'string',
2071+
Database::VAR_TEXT => 'string',
2072+
Database::VAR_MEDIUMTEXT => 'string',
2073+
Database::VAR_LONGTEXT => 'string',
20702074
Database::VAR_INTEGER => 'int',
20712075
Database::VAR_FLOAT => 'double',
20722076
Database::VAR_BOOLEAN => 'bool',
@@ -2694,6 +2698,17 @@ public function getLimitForString(): int
26942698
return 2147483647;
26952699
}
26962700

2701+
/**
2702+
* Get max VARCHAR limit
2703+
* MongoDB doesn't distinguish between string types, so using same as string limit
2704+
*
2705+
* @return int
2706+
*/
2707+
public function getMaxVarcharLength(): int
2708+
{
2709+
return 2147483647;
2710+
}
2711+
26972712
/**
26982713
* Get max INT limit
26992714
*

src/Database/Adapter/Pool.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ public function getMaxIndexLength(): int
287287
return $this->delegate(__FUNCTION__, \func_get_args());
288288
}
289289

290+
public function getMaxVarcharLength(): int
291+
{
292+
return $this->delegate(__FUNCTION__, \func_get_args());
293+
}
294+
290295
public function getMaxUIDLength(): int
291296
{
292297
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1922,6 +1922,14 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
19221922

19231923
return "VARCHAR({$size})";
19241924

1925+
case Database::VAR_VARCHAR:
1926+
return "VARCHAR({$size})";
1927+
1928+
case Database::VAR_TEXT:
1929+
case Database::VAR_MEDIUMTEXT:
1930+
case Database::VAR_LONGTEXT:
1931+
return 'TEXT'; // PostgreSQL doesn't have MEDIUMTEXT/LONGTEXT, use TEXT
1932+
19251933
case Database::VAR_INTEGER: // We don't support zerofill: https://stackoverflow.com/a/5634147/2299554
19261934

19271935
if ($size >= 8) { // INT = 4 bytes, BIGINT = 8 bytes
@@ -1958,7 +1966,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool
19581966
return "VECTOR({$size})";
19591967

19601968
default:
1961-
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);
1969+
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);
19621970
}
19631971
}
19641972

src/Database/Adapter/SQL.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,19 @@ public function getAttributeWidth(Document $collection): int
11441144

11451145
break;
11461146

1147+
case Database::VAR_VARCHAR:
1148+
$total += match (true) {
1149+
$attribute['size'] > 255 => $attribute['size'] * 4 + 2, // VARCHAR(>255) + 2 length
1150+
default => $attribute['size'] * 4 + 1, // VARCHAR(<=255) + 1 length
1151+
};
1152+
break;
1153+
1154+
case Database::VAR_TEXT:
1155+
case Database::VAR_MEDIUMTEXT:
1156+
case Database::VAR_LONGTEXT:
1157+
$total += 20; // Pointer storage for TEXT types
1158+
break;
1159+
11471160
case Database::VAR_INTEGER:
11481161
if ($attribute['size'] >= 8) {
11491162
$total += 8; // BIGINT 8 bytes

src/Database/Database.php

Lines changed: 42 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Utopia\Database\Helpers\ID;
2626
use Utopia\Database\Helpers\Permission;
2727
use Utopia\Database\Helpers\Role;
28+
use Utopia\Database\Validator\Attribute as AttributeValidator;
2829
use Utopia\Database\Validator\Authorization;
2930
use Utopia\Database\Validator\Authorization\Input;
3031
use Utopia\Database\Validator\Index as IndexValidator;
@@ -45,6 +46,11 @@ class Database
4546
public const VAR_BOOLEAN = 'boolean';
4647
public const VAR_DATETIME = 'datetime';
4748

49+
public const VAR_VARCHAR = 'varchar';
50+
public const VAR_TEXT = 'text';
51+
public const VAR_MEDIUMTEXT = 'mediumtext';
52+
public const VAR_LONGTEXT = 'longtext';
53+
4854
// ID types
4955
public const VAR_ID = 'id';
5056
public const VAR_UUID7 = 'uuid7';
@@ -2232,36 +2238,6 @@ private function validateAttribute(
22322238
array $formatOptions,
22332239
array $filters
22342240
): Document {
2235-
// Attribute IDs are case-insensitive
2236-
$attributes = $collection->getAttribute('attributes', []);
2237-
2238-
/** @var array<Document> $attributes */
2239-
foreach ($attributes as $attribute) {
2240-
if (\strtolower($attribute->getId()) === \strtolower($id)) {
2241-
throw new DuplicateException('Attribute already exists in metadata');
2242-
}
2243-
}
2244-
2245-
if ($this->adapter->getSupportForSchemaAttributes() && !($this->getSharedTables() && $this->isMigrating())) {
2246-
$schema = $this->getSchemaAttributes($collection->getId());
2247-
foreach ($schema as $attribute) {
2248-
$newId = $this->adapter->filter($attribute->getId());
2249-
if (\strtolower($newId) === \strtolower($id)) {
2250-
throw new DuplicateException('Attribute already exists in schema');
2251-
}
2252-
}
2253-
}
2254-
2255-
// Ensure required filters for the attribute are passed
2256-
$requiredFilters = $this->getRequiredFilters($type);
2257-
if (!empty(\array_diff($requiredFilters, $filters))) {
2258-
throw new DatabaseException("Attribute of type: $type requires the following filters: " . implode(",", $requiredFilters));
2259-
}
2260-
2261-
if ($format && !Structure::hasFormat($format, $type)) {
2262-
throw new DatabaseException('Format ("' . $format . '") not available for this attribute type ("' . $type . '")');
2263-
}
2264-
22652241
$attribute = new Document([
22662242
'$id' => ID::custom($id),
22672243
'key' => $id,
@@ -2276,111 +2252,31 @@ private function validateAttribute(
22762252
'filters' => $filters,
22772253
]);
22782254

2279-
$this->checkAttribute($collection, $attribute);
2280-
2281-
switch ($type) {
2282-
case self::VAR_ID:
2283-
2284-
break;
2285-
case self::VAR_STRING:
2286-
if ($size > $this->adapter->getLimitForString()) {
2287-
throw new DatabaseException('Max size allowed for string is: ' . number_format($this->adapter->getLimitForString()));
2288-
}
2289-
break;
2290-
case self::VAR_INTEGER:
2291-
$limit = ($signed) ? $this->adapter->getLimitForInt() / 2 : $this->adapter->getLimitForInt();
2292-
if ($size > $limit) {
2293-
throw new DatabaseException('Max size allowed for int is: ' . number_format($limit));
2294-
}
2295-
break;
2296-
case self::VAR_FLOAT:
2297-
case self::VAR_BOOLEAN:
2298-
case self::VAR_DATETIME:
2299-
case self::VAR_RELATIONSHIP:
2300-
break;
2301-
case self::VAR_OBJECT:
2302-
if (!$this->adapter->getSupportForObject()) {
2303-
throw new DatabaseException('Object attributes are not supported');
2304-
}
2305-
if (!empty($size)) {
2306-
throw new DatabaseException('Size must be empty for object attributes');
2307-
}
2308-
if (!empty($array)) {
2309-
throw new DatabaseException('Object attributes cannot be arrays');
2310-
}
2311-
break;
2312-
case self::VAR_POINT:
2313-
case self::VAR_LINESTRING:
2314-
case self::VAR_POLYGON:
2315-
// Check if adapter supports spatial attributes
2316-
if (!$this->adapter->getSupportForSpatialAttributes()) {
2317-
throw new DatabaseException('Spatial attributes are not supported');
2318-
}
2319-
if (!empty($size)) {
2320-
throw new DatabaseException('Size must be empty for spatial attributes');
2321-
}
2322-
if (!empty($array)) {
2323-
throw new DatabaseException('Spatial attributes cannot be arrays');
2324-
}
2325-
break;
2326-
case self::VAR_VECTOR:
2327-
if (!$this->adapter->getSupportForVectors()) {
2328-
throw new DatabaseException('Vector types are not supported by the current database');
2329-
}
2330-
if ($array) {
2331-
throw new DatabaseException('Vector type cannot be an array');
2332-
}
2333-
if ($size <= 0) {
2334-
throw new DatabaseException('Vector dimensions must be a positive integer');
2335-
}
2336-
if ($size > self::MAX_VECTOR_DIMENSIONS) {
2337-
throw new DatabaseException('Vector dimensions cannot exceed ' . self::MAX_VECTOR_DIMENSIONS);
2338-
}
2339-
2340-
// Validate default value if provided
2341-
if ($default !== null) {
2342-
if (!is_array($default)) {
2343-
throw new DatabaseException('Vector default value must be an array');
2344-
}
2345-
if (count($default) !== $size) {
2346-
throw new DatabaseException('Vector default value must have exactly ' . $size . ' elements');
2347-
}
2348-
foreach ($default as $component) {
2349-
if (!is_numeric($component)) {
2350-
throw new DatabaseException('Vector default value must contain only numeric elements');
2351-
}
2352-
}
2353-
}
2354-
break;
2355-
default:
2356-
$supportedTypes = [
2357-
self::VAR_STRING,
2358-
self::VAR_INTEGER,
2359-
self::VAR_FLOAT,
2360-
self::VAR_BOOLEAN,
2361-
self::VAR_DATETIME,
2362-
self::VAR_RELATIONSHIP
2363-
];
2364-
if ($this->adapter->getSupportForVectors()) {
2365-
$supportedTypes[] = self::VAR_VECTOR;
2366-
}
2367-
if ($this->adapter->getSupportForSpatialAttributes()) {
2368-
\array_push($supportedTypes, ...self::SPATIAL_TYPES);
2369-
}
2370-
if ($this->adapter->getSupportForObject()) {
2371-
$supportedTypes[] = self::VAR_OBJECT;
2372-
}
2373-
throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . implode(', ', $supportedTypes));
2374-
}
2375-
2376-
// Only execute when $default is given
2377-
if (!\is_null($default)) {
2378-
if ($required === true) {
2379-
throw new DatabaseException('Cannot set a default value for a required attribute');
2380-
}
2255+
$collectionClone = clone $collection;
2256+
$collectionClone->setAttribute('attributes', $attribute, Document::SET_TYPE_APPEND);
2257+
2258+
$validator = new AttributeValidator(
2259+
attributes: $collection->getAttribute('attributes', []),
2260+
schemaAttributes: $this->adapter->getSupportForSchemaAttributes()
2261+
? $this->getSchemaAttributes($collection->getId())
2262+
: [],
2263+
maxAttributes: $this->adapter->getLimitForAttributes(),
2264+
maxWidth: $this->adapter->getDocumentSizeLimit(),
2265+
maxStringLength: $this->adapter->getLimitForString(),
2266+
maxVarcharLength: $this->adapter->getMaxVarcharLength(),
2267+
maxIntLength: $this->adapter->getLimitForInt(),
2268+
supportForSchemaAttributes: $this->adapter->getSupportForSchemaAttributes(),
2269+
supportForVectors: $this->adapter->getSupportForVectors(),
2270+
supportForSpatialAttributes: $this->adapter->getSupportForSpatialAttributes(),
2271+
supportForObject: $this->adapter->getSupportForObject(),
2272+
attributeCountCallback: fn () => $this->adapter->getCountOfAttributes($collectionClone),
2273+
attributeWidthCallback: fn () => $this->adapter->getAttributeWidth($collectionClone),
2274+
filterCallback: fn ($id) => $this->adapter->filter($id),
2275+
isMigrating: $this->isMigrating(),
2276+
sharedTables: $this->getSharedTables(),
2277+
);
23812278

2382-
$this->validateDefaultTypes($type, $default);
2383-
}
2279+
$validator->isValid($attribute);
23842280

23852281
return $attribute;
23862282
}
@@ -2430,6 +2326,14 @@ protected function validateDefaultTypes(string $type, mixed $default): void
24302326

24312327
switch ($type) {
24322328
case self::VAR_STRING:
2329+
case self::VAR_VARCHAR:
2330+
case self::VAR_TEXT:
2331+
case self::VAR_MEDIUMTEXT:
2332+
case self::VAR_LONGTEXT:
2333+
if ($defaultType !== 'string') {
2334+
throw new DatabaseException('Default value ' . $default . ' does not match given type ' . $type);
2335+
}
2336+
break;
24332337
case self::VAR_INTEGER:
24342338
case self::VAR_FLOAT:
24352339
case self::VAR_BOOLEAN:
@@ -2451,6 +2355,10 @@ protected function validateDefaultTypes(string $type, mixed $default): void
24512355
default:
24522356
$supportedTypes = [
24532357
self::VAR_STRING,
2358+
self::VAR_VARCHAR,
2359+
self::VAR_TEXT,
2360+
self::VAR_MEDIUMTEXT,
2361+
self::VAR_LONGTEXT,
24542362
self::VAR_INTEGER,
24552363
self::VAR_FLOAT,
24562364
self::VAR_BOOLEAN,

0 commit comments

Comments
 (0)