diff --git a/composer.json b/composer.json
index cbf90ee53..4a0fecbd2 100755
--- a/composer.json
+++ b/composer.json
@@ -56,8 +56,8 @@
},
"config": {
"allow-plugins": {
- "php-http/discovery": true,
- "tbachert/spi": true
+ "php-http/discovery": false,
+ "tbachert/spi": false
}
}
}
diff --git a/composer.lock b/composer.lock
index 3abe843d8..774cd790d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -337,16 +337,16 @@
},
{
"name": "open-telemetry/api",
- "version": "1.2.3",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/api.git",
- "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1"
+ "reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/199d7ddda88f5f5619fa73463f1a5a7149ccd1f1",
- "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1",
+ "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/4e3bb38e069876fb73c2ce85c89583bf2b28cd86",
+ "reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86",
"shasum": ""
},
"require": {
@@ -403,20 +403,20 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
- "time": "2025-03-05T21:42:54+00:00"
+ "time": "2025-05-07T12:32:21+00:00"
},
{
"name": "open-telemetry/context",
- "version": "1.2.0",
+ "version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/context.git",
- "reference": "5f553042b951d3fedf47925852c380159dfca801"
+ "reference": "1eb2b837ee9362db064a6b65d5ecce15a9f9f020"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/5f553042b951d3fedf47925852c380159dfca801",
- "reference": "5f553042b951d3fedf47925852c380159dfca801",
+ "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/1eb2b837ee9362db064a6b65d5ecce15a9f9f020",
+ "reference": "1eb2b837ee9362db064a6b65d5ecce15a9f9f020",
"shasum": ""
},
"require": {
@@ -462,20 +462,20 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
- "time": "2025-05-02T01:57:57+00:00"
+ "time": "2025-05-07T23:36:50+00:00"
},
{
"name": "open-telemetry/exporter-otlp",
- "version": "1.2.1",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/exporter-otlp.git",
- "reference": "b7580440b7481a98da97aceabeb46e1b276c8747"
+ "reference": "19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/b7580440b7481a98da97aceabeb46e1b276c8747",
- "reference": "b7580440b7481a98da97aceabeb46e1b276c8747",
+ "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c",
+ "reference": "19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c",
"shasum": ""
},
"require": {
@@ -526,7 +526,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
- "time": "2025-03-06T23:21:56+00:00"
+ "time": "2025-05-12T00:36:35+00:00"
},
{
"name": "open-telemetry/gen-otlp-protobuf",
@@ -593,16 +593,16 @@
},
{
"name": "open-telemetry/sdk",
- "version": "1.3.0",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sdk.git",
- "reference": "05d9ceb6773b5bddcf485af6d4a6f543bbeb980b"
+ "reference": "939d3a28395c249a763676458140dad44b3a8011"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/05d9ceb6773b5bddcf485af6d4a6f543bbeb980b",
- "reference": "05d9ceb6773b5bddcf485af6d4a6f543bbeb980b",
+ "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/939d3a28395c249a763676458140dad44b3a8011",
+ "reference": "939d3a28395c249a763676458140dad44b3a8011",
"shasum": ""
},
"require": {
@@ -679,7 +679,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
- "time": "2025-05-01T23:20:43+00:00"
+ "time": "2025-05-07T12:32:21+00:00"
},
{
"name": "open-telemetry/sem-conv",
@@ -1782,16 +1782,16 @@
},
{
"name": "utopia-php/cache",
- "version": "0.13.0",
+ "version": "0.13.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
- "reference": "dee01dec33a211644d60f6cfa56b1b8176d3fae3"
+ "reference": "97220cb3b3822b166ee016d1646e2ae2815dc540"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/cache/zipball/dee01dec33a211644d60f6cfa56b1b8176d3fae3",
- "reference": "dee01dec33a211644d60f6cfa56b1b8176d3fae3",
+ "url": "https://api.github.com/repos/utopia-php/cache/zipball/97220cb3b3822b166ee016d1646e2ae2815dc540",
+ "reference": "97220cb3b3822b166ee016d1646e2ae2815dc540",
"shasum": ""
},
"require": {
@@ -1828,9 +1828,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
- "source": "https://github.com/utopia-php/cache/tree/0.13.0"
+ "source": "https://github.com/utopia-php/cache/tree/0.13.1"
},
- "time": "2025-04-17T04:20:26+00:00"
+ "time": "2025-05-09T14:43:52+00:00"
},
{
"name": "utopia-php/compression",
@@ -2164,16 +2164,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.22.0",
+ "version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36"
+ "reference": "941d1927c5ca420c22710e98420287169c7bcaf7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
- "reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7",
+ "reference": "941d1927c5ca420c22710e98420287169c7bcaf7",
"shasum": ""
},
"require": {
@@ -2185,11 +2185,11 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75.0",
- "illuminate/view": "^11.44.2",
- "larastan/larastan": "^3.3.1",
+ "illuminate/view": "^11.44.7",
+ "larastan/larastan": "^3.4.0",
"laravel-zero/framework": "^11.36.1",
"mockery/mockery": "^1.6.12",
- "nunomaduro/termwind": "^2.3",
+ "nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
},
"bin": [
@@ -2226,7 +2226,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2025-04-08T22:11:45+00:00"
+ "time": "2025-05-08T08:38:12+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -4131,7 +4131,7 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -4139,6 +4139,6 @@
"ext-pdo": "*",
"ext-mbstring": "*"
},
- "platform-dev": [],
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/phpunit.xml b/phpunit.xml
index ccdaa969e..8ba994793 100755
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -16,14 +16,4 @@
./tests/e2e/Adapter
-
-
- ./src/
- ./tests/
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php
index 9b25dae55..89ae9feb6 100644
--- a/src/Database/Adapter.php
+++ b/src/Database/Adapter.php
@@ -536,6 +536,17 @@ abstract public function analyzeCollection(string $collection): bool;
*/
abstract public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool;
+ /**
+ * Create Attributes
+ *
+ * @param string $collection
+ * @param array> $attributes
+ * @return bool
+ * @throws TimeoutException
+ * @throws DuplicateException
+ */
+ abstract public function createAttributes(string $collection, array $attributes): bool;
+
/**
* Update Attribute
*
@@ -988,6 +999,13 @@ abstract public function getSupportForReconnection(): bool;
*/
abstract public function getSupportForHostname(): bool;
+ /**
+ * Is creating multiple attributes in a single query supported?
+ *
+ * @return bool
+ */
+ abstract public function getSupportForBatchCreateAttributes(): bool;
+
/**
* Get current attribute count from collection document
*
@@ -1094,7 +1112,7 @@ public function filter(string $value): string
return $value;
}
- public function escapeWildcards(string $value): string
+ protected function escapeWildcards(string $value): string
{
$wildcards = [
'%',
diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php
index 9b3d0c126..e9c639536 100644
--- a/src/Database/Adapter/MariaDB.php
+++ b/src/Database/Adapter/MariaDB.php
@@ -358,33 +358,51 @@ public function analyzeCollection(string $collection): bool
}
/**
- * Create Attribute
+ * Get Schema Attributes
*
* @param string $collection
- * @param string $id
- * @param string $type
- * @param int $size
- * @param bool $signed
- * @param bool $array
- * @return bool
- * @throws Exception
- * @throws PDOException
+ * @return array
+ * @throws DatabaseException
*/
- public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool
+ public function getSchemaAttributes(string $collection): array
{
- $name = $this->filter($collection);
- $id = $this->filter($id);
- $type = $this->getSQLType($type, $size, $signed, $array);
-
- $sql = "ALTER TABLE {$this->getSQLTable($name)} ADD COLUMN `{$id}` {$type};";
- $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql);
+ $schema = $this->getDatabase();
+ $collection = $this->getNamespace().'_'.$this->filter($collection);
try {
- return $this->getPDO()
- ->prepare($sql)
- ->execute();
+ $stmt = $this->getPDO()->prepare('
+ SELECT
+ COLUMN_NAME as _id,
+ COLUMN_DEFAULT as columnDefault,
+ IS_NULLABLE as isNullable,
+ DATA_TYPE as dataType,
+ CHARACTER_MAXIMUM_LENGTH as characterMaximumLength,
+ NUMERIC_PRECISION as numericPrecision,
+ NUMERIC_SCALE as numericScale,
+ DATETIME_PRECISION as datetimePrecision,
+ COLUMN_TYPE as columnType,
+ COLUMN_KEY as columnKey,
+ EXTRA as extra
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table
+ ');
+ $stmt->bindParam(':schema', $schema);
+ $stmt->bindParam(':table', $collection);
+ $stmt->execute();
+ $results = $stmt->fetchAll();
+ $stmt->closeCursor();
+
+ foreach ($results as $index => $document) {
+ $document['$id'] = $document['_id'];
+ unset($document['_id']);
+
+ $results[$index] = new Document($document);
+ }
+
+ return $results;
+
} catch (PDOException $e) {
- throw $this->processException($e);
+ throw new DatabaseException('Failed to get schema attributes', $e->getCode(), $e);
}
}
@@ -425,67 +443,6 @@ public function updateAttribute(string $collection, string $id, string $type, in
}
}
- /**
- * Delete Attribute
- *
- * @param string $collection
- * @param string $id
- * @param bool $array
- * @return bool
- * @throws Exception
- * @throws PDOException
- */
- public function deleteAttribute(string $collection, string $id, bool $array = false): bool
- {
- $name = $this->filter($collection);
- $id = $this->filter($id);
-
- $sql = "ALTER TABLE {$this->getSQLTable($name)} DROP COLUMN `{$id}`;";
-
- $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql);
-
- try {
- return $this->getPDO()
- ->prepare($sql)
- ->execute();
- } catch (PDOException $e) {
- if ($e->getCode() === "42000" && $e->errorInfo[1] === 1091) {
- return true;
- }
-
- throw $this->processException($e);
- }
- }
-
- /**
- * Rename Attribute
- *
- * @param string $collection
- * @param string $old
- * @param string $new
- * @return bool
- * @throws Exception
- * @throws PDOException
- */
- public function renameAttribute(string $collection, string $old, string $new): bool
- {
- $collection = $this->filter($collection);
- $old = $this->filter($old);
- $new = $this->filter($new);
-
- $sql = "ALTER TABLE {$this->getSQLTable($collection)} RENAME COLUMN `{$old}` TO `{$new}`;";
-
- $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql);
-
- try {
- return $this->getPDO()
- ->prepare($sql)
- ->execute();
- } catch (PDOException $e) {
- throw $this->processException($e);
- }
- }
-
/**
* @param string $collection
* @param string $id
@@ -2206,6 +2163,11 @@ public function getSupportForUpserts(): bool
return true;
}
+ public function getSupportForSchemaAttributes(): bool
+ {
+ return true;
+ }
+
/**
* Set max execution time
* @param int $milliseconds
@@ -2295,58 +2257,12 @@ protected function processException(PDOException $e): \Exception
return new NotFoundException('Collection not found', $e->getCode(), $e);
}
- return $e;
- }
-
- /**
- * Get Schema Attributes
- *
- * @param string $collection
- * @return array
- * @throws DatabaseException
- */
- public function getSchemaAttributes(string $collection): array
- {
- $schema = $this->getDatabase();
- $collection = $this->getNamespace().'_'.$this->filter($collection);
-
- try {
- $stmt = $this->getPDO()->prepare('
- SELECT
- COLUMN_NAME as columnName,
- COLUMN_DEFAULT as columnDefault,
- IS_NULLABLE as isNullable,
- DATA_TYPE as dataType,
- CHARACTER_MAXIMUM_LENGTH as characterMaximumLength,
- NUMERIC_PRECISION as numericPrecision,
- NUMERIC_SCALE as numericScale,
- DATETIME_PRECISION as datetimePrecision,
- COLUMN_TYPE as columnType,
- COLUMN_KEY as columnKey,
- EXTRA as extra
- FROM INFORMATION_SCHEMA.COLUMNS
- WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table
- ');
- $stmt->bindParam(':schema', $schema);
- $stmt->bindParam(':table', $collection);
- $stmt->execute();
- $results = $stmt->fetchAll();
- $stmt->closeCursor();
-
- foreach ($results as $index => $document) {
- $results[$index] = new Document($document);
- }
-
- return $results;
-
- } catch (PDOException $e) {
- throw new DatabaseException('Failed to get schema attributes', $e->getCode(), $e);
+ // Unknown column
+ if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1091) {
+ return new NotFoundException('Attribute not found', $e->getCode(), $e);
}
- }
- public function getSupportForSchemaAttributes(): bool
- {
- return true;
+ return $e;
}
protected function quote(string $string): string
diff --git a/src/Database/Adapter/Pool.php b/src/Database/Adapter/Pool.php
index 86521df0a..084bd2768 100644
--- a/src/Database/Adapter/Pool.php
+++ b/src/Database/Adapter/Pool.php
@@ -170,6 +170,11 @@ public function createAttribute(string $collection, string $id, string $type, in
return $this->delegate(__FUNCTION__, \func_get_args());
}
+ public function createAttributes(string $collection, array $attributes): bool
+ {
+ return $this->delegate(__FUNCTION__, \func_get_args());
+ }
+
public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
@@ -410,6 +415,11 @@ public function getSupportForHostname(): bool
return $this->delegate(__FUNCTION__, \func_get_args());
}
+ public function getSupportForBatchCreateAttributes(): bool
+ {
+ return $this->delegate(__FUNCTION__, \func_get_args());
+ }
+
public function getCountOfAttributes(Document $collection): int
{
return $this->delegate(__FUNCTION__, \func_get_args());
diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php
index 5d94c4ad9..76adf39d3 100644
--- a/src/Database/Adapter/Postgres.php
+++ b/src/Database/Adapter/Postgres.php
@@ -10,6 +10,7 @@
use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
+use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Exception\Transaction as TransactionException;
@@ -401,76 +402,6 @@ public function analyzeCollection(string $collection): bool
return false;
}
- /**
- * Create Attribute
- *
- * @param string $collection
- * @param string $id
- * @param string $type
- * @param int $size
- * @param bool $signed
- * @param bool $array
- *
- * @return bool
- * @throws DatabaseException
- */
- public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool
- {
- $name = $this->filter($collection);
- $id = $this->filter($id);
- $type = $this->getSQLType($type, $size, $signed, $array);
-
- $sql = "
- ALTER TABLE {$this->getSQLTable($name)}
- ADD COLUMN \"{$id}\" {$type}
- ";
-
- $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql);
-
- try {
- return $this->getPDO()
- ->prepare($sql)
- ->execute();
- } catch (PDOException $e) {
- throw $this->processException($e);
- }
- }
-
- /**
- * Delete Attribute
- *
- * @param string $collection
- * @param string $id
- * @param bool $array
- *
- * @return bool
- * @throws DatabaseException
- */
- public function deleteAttribute(string $collection, string $id, bool $array = false): bool
- {
- $name = $this->filter($collection);
- $id = $this->filter($id);
-
- $sql = "
- ALTER TABLE {$this->getSQLTable($name)}
- DROP COLUMN \"{$id}\";
- ";
-
- $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql);
-
- try {
- return $this->getPDO()
- ->prepare($sql)
- ->execute();
- } catch (PDOException $e) {
- if ($e->getCode() === "42703" && $e->errorInfo[1] === 7) {
- return true;
- }
-
- throw $e;
- }
- }
-
/**
* Rename Attribute
*
@@ -1996,17 +1927,6 @@ protected function getSQLSchema(): string
return "\"{$this->getDatabase()}\".";
}
- /**
- * Get SQL table
- *
- * @param string $name
- * @return string
- */
- protected function getSQLTable(string $name): string
- {
- return "\"{$this->getDatabase()}\".\"{$this->getNamespace()}_{$name}\"";
- }
-
/**
* Get PDO Type
*
@@ -2148,6 +2068,11 @@ protected function processException(PDOException $e): \Exception
return new TruncateException('Resize would result in data truncation', $e->getCode(), $e);
}
+ // Unknown column
+ if ($e->getCode() === "42703" && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
+ return new NotFoundException('Attribute not found', $e->getCode(), $e);
+ }
+
return $e;
}
diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php
index c2907b88d..f88fec46f 100644
--- a/src/Database/Adapter/SQL.php
+++ b/src/Database/Adapter/SQL.php
@@ -200,6 +200,126 @@ public function list(): array
return [];
}
+ /**
+ * Create Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ * @param string $type
+ * @param int $size
+ * @param bool $signed
+ * @param bool $array
+ * @return bool
+ * @throws Exception
+ * @throws PDOException
+ */
+ public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool
+ {
+ $id = $this->quote($this->filter($id));
+ $type = $this->getSQLType($type, $size, $signed, $array);
+
+ $sql = "ALTER TABLE {$this->getSQLTable($collection)} ADD COLUMN {$id} {$type};";
+ $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql);
+
+ try {
+ return $this->getPDO()
+ ->prepare($sql)
+ ->execute();
+ } catch (PDOException $e) {
+ throw $this->processException($e);
+ }
+ }
+
+ /**
+ * Create Attributes
+ *
+ * @param string $collection
+ * @param array> $attributes
+ * @return bool
+ * @throws DatabaseException
+ */
+ public function createAttributes(string $collection, array $attributes): bool
+ {
+ $parts = [];
+ foreach ($attributes as $attribute) {
+ $id = $this->quote($this->filter($attribute['$id']));
+ $type = $this->getSQLType(
+ $attribute['type'],
+ $attribute['size'],
+ $attribute['signed'] ?? true,
+ $attribute['array'] ?? false,
+ );
+ $parts[] = "{$id} {$type}";
+ }
+
+ $columns = \implode(', ADD COLUMN ', $parts);
+
+ $sql = "ALTER TABLE {$this->getSQLTable($collection)} ADD COLUMN {$columns};";
+ $sql = $this->trigger(Database::EVENT_ATTRIBUTE_CREATE, $sql);
+
+ try {
+ return $this->getPDO()
+ ->prepare($sql)
+ ->execute();
+ } catch (PDOException $e) {
+ throw $this->processException($e);
+ }
+ }
+
+ /**
+ * Rename Attribute
+ *
+ * @param string $collection
+ * @param string $old
+ * @param string $new
+ * @return bool
+ * @throws Exception
+ * @throws PDOException
+ */
+ public function renameAttribute(string $collection, string $old, string $new): bool
+ {
+ $collection = $this->filter($collection);
+ $old = $this->quote($this->filter($old));
+ $new = $this->quote($this->filter($new));
+
+ $sql = "ALTER TABLE {$this->getSQLTable($collection)} RENAME COLUMN {$old} TO {$new};";
+
+ $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql);
+
+ try {
+ return $this->getPDO()
+ ->prepare($sql)
+ ->execute();
+ } catch (PDOException $e) {
+ throw $this->processException($e);
+ }
+ }
+
+ /**
+ * Delete Attribute
+ *
+ * @param string $collection
+ * @param string $id
+ * @param bool $array
+ * @return bool
+ * @throws Exception
+ * @throws PDOException
+ */
+ public function deleteAttribute(string $collection, string $id, bool $array = false): bool
+ {
+ $id = $this->quote($this->filter($id));
+ $sql = "ALTER TABLE {$this->getSQLTable($collection)} DROP COLUMN {$id};";
+ $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql);
+
+ try {
+ return $this->getPDO()
+ ->prepare($sql)
+ ->execute();
+ } catch (PDOException $e) {
+ throw $this->processException($e);
+ }
+ }
+
/**
* Get Document
*
@@ -1261,6 +1381,11 @@ public function getSupportForReconnection(): bool
return true;
}
+ public function getSupportForBatchCreateAttributes(): bool
+ {
+ return true;
+ }
+
/**
* @param string $value
* @return string
@@ -1324,16 +1449,12 @@ protected function getSQLOperator(string $method): string
}
}
- public function escapeWildcards(string $value): string
- {
- $wildcards = ['%', '_', '[', ']', '^', '-', '.', '*', '+', '?', '(', ')', '{', '}', '|'];
-
- foreach ($wildcards as $wildcard) {
- $value = \str_replace($wildcard, "\\$wildcard", $value);
- }
-
- return $value;
- }
+ abstract protected function getSQLType(
+ string $type,
+ int $size,
+ bool $signed = true,
+ bool $array = false
+ ): string;
/**
* Get SQL Index Type
@@ -1616,6 +1737,17 @@ protected function getInternalKeyForAttribute(string $attribute): string
};
}
+ protected function escapeWildcards(string $value): string
+ {
+ $wildcards = ['%', '_', '[', ']', '^', '-', '.', '*', '+', '?', '(', ')', '{', '}', '|'];
+
+ foreach ($wildcards as $wildcard) {
+ $value = \str_replace($wildcard, "\\$wildcard", $value);
+ }
+
+ return $value;
+ }
+
protected function processException(PDOException $e): \Exception
{
return $e;
diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php
index e9a7ecabc..dc1cbd900 100644
--- a/src/Database/Adapter/SQLite.php
+++ b/src/Database/Adapter/SQLite.php
@@ -948,6 +948,16 @@ public function getSupportForHostname(): bool
return false;
}
+ /**
+ * Is batch create attributes supported?
+ *
+ * @return bool
+ */
+ public function getSupportForBatchCreateAttributes(): bool
+ {
+ return false;
+ }
+
/**
* Get SQL Index Type
*
@@ -1049,7 +1059,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles,
*/
protected function getSQLTable(string $name): string
{
- return "`{$this->getNamespace()}_{$name}`";
+ return $this->quote("{$this->getNamespace()}_{$this->filter($name)}");
}
/**
diff --git a/src/Database/Database.php b/src/Database/Database.php
index 9480ceb5e..6e2295185 100644
--- a/src/Database/Database.php
+++ b/src/Database/Database.php
@@ -136,6 +136,7 @@ class Database
public const EVENT_PERMISSIONS_DELETE = 'permissions_delete';
public const EVENT_ATTRIBUTE_CREATE = 'attribute_create';
+ public const EVENT_ATTRIBUTES_CREATE = 'attributes_create';
public const EVENT_ATTRIBUTE_UPDATE = 'attribute_update';
public const EVENT_ATTRIBUTE_DELETE = 'attribute_delete';
@@ -1544,12 +1545,206 @@ public function createAttribute(string $collection, string $id, string $type, in
throw new NotFoundException('Collection not found');
}
+ $attribute = $this->validateAttribute(
+ $collection,
+ $id,
+ $type,
+ $size,
+ $required,
+ $default,
+ $signed,
+ $array,
+ $format,
+ $formatOptions,
+ $filters
+ );
+
+ $collection->setAttribute(
+ 'attributes',
+ $attribute,
+ Document::SET_TYPE_APPEND
+ );
+
+ try {
+ $created = $this->adapter->createAttribute($collection->getId(), $id, $type, $size, $signed, $array);
+
+ if (!$created) {
+ throw new DatabaseException('Failed to create attribute');
+ }
+ } catch (DuplicateException $e) {
+ // HACK: Metadata should still be updated, can be removed when null tenant collections are supported.
+ if (!$this->adapter->getSharedTables() || !$this->isMigrating()) {
+ throw $e;
+ }
+ }
+
+ if ($collection->getId() !== self::METADATA) {
+ $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+ }
+
+ $this->purgeCachedCollection($collection->getId());
+ $this->purgeCachedDocument(self::METADATA, $collection->getId());
+
+ $this->trigger(self::EVENT_ATTRIBUTE_CREATE, $attribute);
+
+ return true;
+ }
+
+ /**
+ * Create Attribute
+ *
+ * @param string $collection
+ * @param array> $attributes
+ * @return bool
+ * @throws AuthorizationException
+ * @throws ConflictException
+ * @throws DatabaseException
+ * @throws DuplicateException
+ * @throws LimitException
+ * @throws StructureException
+ * @throws Exception
+ */
+ public function createAttributes(string $collection, array $attributes): bool
+ {
+ if (empty($attributes)) {
+ throw new DatabaseException('No attributes to create');
+ }
+
+ $collection = $this->silent(fn () => $this->getCollection($collection));
+
+ if ($collection->isEmpty()) {
+ throw new NotFoundException('Collection not found');
+ }
+
+ $attributeDocuments = [];
+ foreach ($attributes as $attribute) {
+ if (!isset($attribute['$id'])) {
+ throw new DatabaseException('Missing attribute key');
+ }
+ if (!isset($attribute['type'])) {
+ throw new DatabaseException('Missing attribute type');
+ }
+ if (!isset($attribute['size'])) {
+ throw new DatabaseException('Missing attribute size');
+ }
+ if (!isset($attribute['required'])) {
+ throw new DatabaseException('Missing attribute required');
+ }
+ if (!isset($attribute['default'])) {
+ $attribute['default'] = null;
+ }
+ if (!isset($attribute['signed'])) {
+ $attribute['signed'] = true;
+ }
+ if (!isset($attribute['array'])) {
+ $attribute['array'] = false;
+ }
+ if (!isset($attribute['format'])) {
+ $attribute['format'] = null;
+ }
+ if (!isset($attribute['formatOptions'])) {
+ $attribute['formatOptions'] = [];
+ }
+ if (!isset($attribute['filters'])) {
+ $attribute['filters'] = [];
+ }
+
+ $attributeDocument = $this->validateAttribute(
+ $collection,
+ $attribute['$id'],
+ $attribute['type'],
+ $attribute['size'],
+ $attribute['required'],
+ $attribute['default'],
+ $attribute['signed'],
+ $attribute['array'],
+ $attribute['format'],
+ $attribute['formatOptions'],
+ $attribute['filters']
+ );
+
+ $collection->setAttribute(
+ 'attributes',
+ $attributeDocument,
+ Document::SET_TYPE_APPEND
+ );
+
+ $attributeDocuments[] = $attributeDocument;
+ }
+
+ try {
+ $created = $this->adapter->createAttributes($collection->getId(), $attributes);
+
+ if (!$created) {
+ throw new DatabaseException('Failed to create attributes');
+ }
+ } catch (DuplicateException $e) {
+ // No attributes were in a metadata, but at least one of them was present on the table
+ // HACK: Metadata should still be updated, can be removed when null tenant collections are supported.
+ if (!$this->adapter->getSharedTables() || !$this->isMigrating()) {
+ throw $e;
+ }
+ }
+
+ if ($collection->getId() !== self::METADATA) {
+ $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
+ }
+
+ $this->purgeCachedCollection($collection->getId());
+ $this->purgeCachedDocument(self::METADATA, $collection->getId());
+
+ $this->trigger(self::EVENT_ATTRIBUTE_CREATE, $attributeDocuments);
+
+ return true;
+ }
+
+ /**
+ * @param Document $collection
+ * @param string $id
+ * @param string $type
+ * @param int $size
+ * @param bool $required
+ * @param mixed $default
+ * @param bool $signed
+ * @param bool $array
+ * @param string $format
+ * @param array $formatOptions
+ * @param array $filters
+ * @return Document
+ * @throws DuplicateException
+ * @throws LimitException
+ * @throws Exception
+ */
+ private function validateAttribute(
+ Document $collection,
+ string $id,
+ string $type,
+ int $size,
+ bool $required,
+ mixed $default,
+ bool $signed,
+ bool $array,
+ ?string $format,
+ 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');
+ 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');
+ }
}
}
@@ -1579,12 +1774,6 @@ public function createAttribute(string $collection, string $id, string $type, in
$this->checkAttribute($collection, $attribute);
- $collection->setAttribute(
- 'attributes',
- $attribute,
- Document::SET_TYPE_APPEND
- );
-
switch ($type) {
case self::VAR_STRING:
if ($size > $this->adapter->getLimitForString()) {
@@ -1615,29 +1804,7 @@ public function createAttribute(string $collection, string $id, string $type, in
$this->validateDefaultTypes($type, $default);
}
- try {
- $created = $this->adapter->createAttribute($collection->getId(), $id, $type, $size, $signed, $array);
-
- if (!$created) {
- throw new DatabaseException('Failed to create attribute');
- }
- } catch (DuplicateException $e) {
- // HACK: Metadata should still be updated, can be removed when null tenant collections are supported.
- if (!$this->adapter->getSharedTables() || !$this->isMigrating()) {
- throw $e;
- }
- }
-
- if ($collection->getId() !== self::METADATA) {
- $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection));
- }
-
- $this->purgeCachedCollection($collection->getId());
- $this->purgeCachedDocument(self::METADATA, $collection->getId());
-
- $this->trigger(self::EVENT_ATTRIBUTE_CREATE, $attribute);
-
- return true;
+ return $attribute;
}
/**
@@ -2130,10 +2297,12 @@ public function deleteAttribute(string $collection, string $id): bool
}
}
- $deleted = $this->adapter->deleteAttribute($collection->getId(), $id);
-
- if (!$deleted) {
- throw new DatabaseException('Failed to delete attribute');
+ try {
+ if (!$this->adapter->deleteAttribute($collection->getId(), $id)) {
+ throw new DatabaseException('Failed to delete attribute');
+ }
+ } catch (NotFoundException) {
+ // Ignore
}
$collection->setAttribute('attributes', \array_values($attributes));
diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php
index 2a8ad4dbe..168a227ee 100644
--- a/src/Database/Mirror.php
+++ b/src/Database/Mirror.php
@@ -356,6 +356,40 @@ public function createAttribute(string $collection, string $id, string $type, in
return $result;
}
+ public function createAttributes(string $collection, array $attributes): bool
+ {
+ $result = $this->source->createAttributes($collection, $attributes);
+
+ if ($this->destination === null) {
+ return $result;
+ }
+
+ try {
+ foreach ($attributes as &$attribute) {
+ foreach ($this->writeFilters as $filter) {
+ $document = $filter->beforeCreateAttribute(
+ source: $this->source,
+ destination: $this->destination,
+ collectionId: $collection,
+ attributeId: $attribute['$id'],
+ attribute: new Document($attribute),
+ );
+
+ $attribute = $document->getArrayCopy();
+ }
+ }
+
+ $result = $this->destination->createAttributes(
+ $collection,
+ $attributes,
+ );
+ } catch (\Throwable $err) {
+ $this->logError('createAttributes', $err);
+ }
+
+ return $result;
+ }
+
public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document
{
$document = $this->source->updateAttribute(
diff --git a/tests/e2e/Adapter/Scopes/AttributeTests.php b/tests/e2e/Adapter/Scopes/AttributeTests.php
index cab177400..18876766d 100644
--- a/tests/e2e/Adapter/Scopes/AttributeTests.php
+++ b/tests/e2e/Adapter/Scopes/AttributeTests.php
@@ -7,6 +7,7 @@
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
+use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Dependency as DependencyException;
@@ -26,10 +27,11 @@
trait AttributeTests
{
- public function createRandomString(int $length = 10): string
+ private function createRandomString(int $length = 10): string
{
return \substr(\bin2hex(\random_bytes(\max(1, \intval(($length + 1) / 2)))), 0, $length);
}
+
/**
* Using phpunit dataProviders to check that all these combinations of types/defaults throw exceptions
* https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers
@@ -1628,4 +1630,369 @@ public function testUnknownFormat(): void
$this->expectException(\Exception::class);
$this->assertEquals(false, static::getDatabase()->createAttribute('attributes', 'bad_format', Database::VAR_STRING, 256, true, null, true, false, 'url'));
}
+
+
+ // Bulk attribute creation tests
+ public function testCreateAttributesEmpty(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, []);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesMissingId(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ 'type' => Database::VAR_STRING,
+ 'size' => 10,
+ 'required' => false
+ ]];
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesMissingType(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'size' => 10,
+ 'required' => false
+ ]];
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesMissingSize(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => Database::VAR_STRING,
+ 'required' => false
+ ]];
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesMissingRequired(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => Database::VAR_STRING,
+ 'size' => 10
+ ]];
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesDuplicateMetadata(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+ static::getDatabase()->createAttribute(__FUNCTION__, 'dup', Database::VAR_STRING, 10, false);
+
+ $attributes = [[
+ '$id' => 'dup',
+ 'type' => Database::VAR_STRING,
+ 'size' => 10,
+ 'required' => false
+ ]];
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DuplicateException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DuplicateException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesInvalidFilter(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'date',
+ 'type' => Database::VAR_DATETIME,
+ 'size' => 0,
+ 'required' => false,
+ 'filters' => []
+ ]];
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesInvalidFormat(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => Database::VAR_STRING,
+ 'size' => 10,
+ 'required' => false,
+ 'format' => 'nonexistent'
+ ]];
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesDefaultOnRequired(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => Database::VAR_STRING,
+ 'size' => 10,
+ 'required' => true,
+ 'default' => 'bar'
+ ]];
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesUnknownType(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => 'unknown',
+ 'size' => 0,
+ 'required' => false
+ ]];
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesStringSizeLimit(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $max = static::getDatabase()->getAdapter()->getLimitForString();
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => Database::VAR_STRING,
+ 'size' => $max + 1,
+ 'required' => false
+ ]];
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesIntegerSizeLimit(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $limit = static::getDatabase()->getAdapter()->getLimitForInt() / 2;
+
+ $attributes = [[
+ '$id' => 'foo',
+ 'type' => Database::VAR_INTEGER,
+ 'size' => (int)$limit + 1,
+ 'required' => false
+ ]];
+
+ try {
+ static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->fail('Expected DatabaseException not thrown');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(DatabaseException::class, $e);
+ }
+ }
+
+ public function testCreateAttributesSuccessMultiple(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [
+ [
+ '$id' => 'a',
+ 'type' => Database::VAR_STRING,
+ 'size' => 10,
+ 'required' => false
+ ],
+ [
+ '$id' => 'b',
+ 'type' => Database::VAR_INTEGER,
+ 'size' => 0,
+ 'required' => false
+ ],
+ ];
+
+ $result = static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->assertTrue($result);
+
+ $collection = static::getDatabase()->getCollection(__FUNCTION__);
+ $attrs = $collection->getAttribute('attributes');
+ $this->assertCount(2, $attrs);
+ $this->assertEquals('a', $attrs[0]['$id']);
+ $this->assertEquals('b', $attrs[1]['$id']);
+
+ $doc = static::getDatabase()->createDocument(__FUNCTION__, new Document([
+ 'a' => 'foo',
+ 'b' => 123,
+ ]));
+
+ $this->assertEquals('foo', $doc->getAttribute('a'));
+ $this->assertEquals(123, $doc->getAttribute('b'));
+ }
+
+ public function testCreateAttributesDelete(): void
+ {
+ if (!static::getDatabase()->getAdapter()->getSupportForBatchCreateAttributes()) {
+ $this->expectNotToPerformAssertions();
+ return;
+ }
+
+ static::getDatabase()->createCollection(__FUNCTION__);
+
+ $attributes = [
+ [
+ '$id' => 'a',
+ 'type' => Database::VAR_STRING,
+ 'size' => 10,
+ 'required' => false
+ ],
+ [
+ '$id' => 'b',
+ 'type' => Database::VAR_INTEGER,
+ 'size' => 0,
+ 'required' => false
+ ],
+ ];
+
+ $result = static::getDatabase()->createAttributes(__FUNCTION__, $attributes);
+ $this->assertTrue($result);
+
+ $collection = static::getDatabase()->getCollection(__FUNCTION__);
+ $attrs = $collection->getAttribute('attributes');
+ $this->assertCount(2, $attrs);
+ $this->assertEquals('a', $attrs[0]['$id']);
+ $this->assertEquals('b', $attrs[1]['$id']);
+
+ static::getDatabase()->deleteAttribute(__FUNCTION__, 'a');
+
+ $collection = static::getDatabase()->getCollection(__FUNCTION__);
+ $attrs = $collection->getAttribute('attributes');
+ $this->assertCount(1, $attrs);
+ $this->assertEquals('b', $attrs[0]['$id']);
+ }
}
diff --git a/tests/e2e/Adapter/Scopes/CollectionTests.php b/tests/e2e/Adapter/Scopes/CollectionTests.php
index 6f8762feb..3650ab837 100644
--- a/tests/e2e/Adapter/Scopes/CollectionTests.php
+++ b/tests/e2e/Adapter/Scopes/CollectionTests.php
@@ -506,31 +506,32 @@ public function testSchemaAttributes(): void
/**
* @var Document $attribute
*/
- $attributes[$attribute->getAttribute('columnName')] = $attribute;
+
+ $attributes[$attribute->getId()] = $attribute;
}
$attribute = $attributes['username'];
- $this->assertEquals('username', $attribute['columnName']);
+ $this->assertEquals('username', $attribute['$id']);
$this->assertEquals('varchar', $attribute['dataType']);
$this->assertEquals('varchar(128)', $attribute['columnType']);
$this->assertEquals('128', $attribute['characterMaximumLength']);
$this->assertEquals('YES', $attribute['isNullable']);
$attribute = $attributes['story'];
- $this->assertEquals('story', $attribute['columnName']);
+ $this->assertEquals('story', $attribute['$id']);
$this->assertEquals('text', $attribute['dataType']);
$this->assertEquals('text', $attribute['columnType']);
$this->assertEquals('65535', $attribute['characterMaximumLength']);
$attribute = $attributes['string_list'];
- $this->assertEquals('string_list', $attribute['columnName']);
+ $this->assertEquals('string_list', $attribute['$id']);
$this->assertTrue(in_array($attribute['dataType'], ['json', 'longtext'])); // mysql vs maria
$this->assertTrue(in_array($attribute['columnType'], ['json', 'longtext']));
$this->assertTrue(in_array($attribute['characterMaximumLength'], [null, '4294967295']));
$this->assertEquals('YES', $attribute['isNullable']);
$attribute = $attributes['dob'];
- $this->assertEquals('dob', $attribute['columnName']);
+ $this->assertEquals('dob', $attribute['$id']);
$this->assertEquals('datetime', $attribute['dataType']);
$this->assertEquals('datetime(3)', $attribute['columnType']);
$this->assertEquals(null, $attribute['characterMaximumLength']);
@@ -538,7 +539,7 @@ public function testSchemaAttributes(): void
if ($db->getSharedTables()) {
$attribute = $attributes['_tenant'];
- $this->assertEquals('_tenant', $attribute['columnName']);
+ $this->assertEquals('_tenant', $attribute['$id']);
$this->assertEquals('int', $attribute['dataType']);
$this->assertEquals('10', $attribute['numericPrecision']);
$this->assertTrue(in_array($attribute['columnType'], ['int unsigned', 'int(11) unsigned']));
@@ -784,7 +785,7 @@ public function testKeywords(): void
// Attribute name tests
foreach ($keywords as $keyword) {
- $collectionName = 'rk' . $keyword; // rk is short-hand for reserved-keyword. We do this sicne there are some limits (64 chars max)
+ $collectionName = 'rk' . $keyword; // rk is shorthand for reserved-keyword. We do this since there are some limits (64 chars max)
$collection = $database->createCollection($collectionName);
$this->assertEquals($collectionName, $collection->getId());