diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 970fc22de..f82d92b3d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,40 +1,100 @@
name: "Tests"
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ IMAGE: databases-dev
+ CACHE_KEY: databases-dev-${{ github.event.pull_request.head.sha }}
+
on: [pull_request]
+
jobs:
- tests:
- name: Unit & E2E
+ setup:
+ name: Setup & Build Docker Image
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Build Docker Image
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ push: false
+ tags: ${{ env.IMAGE }}
+ load: true
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
+
+ - name: Cache Docker Image
+ uses: actions/cache@v3
+ with:
+ key: ${{ env.CACHE_KEY }}
+ path: /tmp/${{ env.IMAGE }}.tar
+
+ unit_test:
+ name: Unit Test
runs-on: ubuntu-latest
+ needs: setup
steps:
- - name: Checkout repository
- uses: actions/checkout@v3
- with:
- fetch-depth: 2
- submodules: recursive
-
- - run: git checkout HEAD^2
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
-
- - name: Build image
- uses: docker/build-push-action@v3
- with:
- context: .
- push: false
- tags: database-dev
- load: true
- cache-from: type=gha
- cache-to: type=gha,mode=max
-
- - name: Start Databases
- run: |
- docker compose up -d
- sleep 10
-
- - name: Run Tests
- run: docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml
-
- - name: Check Coverage
- run: docker compose exec -T tests vendor/bin/coverage-check tmp/clover.xml 90
\ No newline at end of file
+ - name: checkout
+ uses: actions/checkout@v2
+
+ - name: Load Cache
+ uses: actions/cache@v3
+ with:
+ key: ${{ env.CACHE_KEY }}
+ path: /tmp/${{ env.IMAGE }}.tar
+ fail-on-cache-miss: true
+
+ - name: Load and Start Services
+ run: |
+ docker load --input /tmp/${{ env.IMAGE }}.tar
+ docker compose up -d
+ sleep 10
+
+ - name: Run Unit Tests
+ run: docker compose exec tests vendor/bin/phpunit /usr/src/code/tests/unit
+
+ adapter_test:
+ name: Adapter Tests
+ runs-on: ubuntu-latest
+ needs: setup
+ strategy:
+ fail-fast: false
+ matrix:
+ adapter:
+ [
+ MariaDB,
+ MySQL,
+ Postgres,
+ SQLite,
+ MongoDB,
+ ]
+
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+
+ - name: Load Cache
+ uses: actions/cache@v3
+ with:
+ key: ${{ env.CACHE_KEY }}
+ path: /tmp/${{ env.IMAGE }}.tar
+ fail-on-cache-miss: true
+
+ - name: Load and Start Services
+ run: |
+ docker load --input /tmp/${{ env.IMAGE }}.tar
+ docker compose up -d
+ sleep 10
+
+ - name: Run ${{matrix.service}} Tests
+ run: docker compose exec -T tests vendor/bin/phpunit /usr/src/code/tests/e2e/Adapter/${{matrix.adapter}}Test.php --debug
\ No newline at end of file
diff --git a/README.md b/README.md
index f9792d101..1adf58143 100644
--- a/README.md
+++ b/README.md
@@ -244,10 +244,10 @@ $database->setNamespace(
);
// Get default database
-$database->getDefaultDatabase();
+$database->getDatabase();
// Sets default database
-$database->setDefaultDatabase(
+$database->setDatabase(
name: 'dbName'
);
@@ -402,7 +402,7 @@ $database->deleteCollection(
);
// Delete cached documents of a collection
-$database->deleteCachedCollection(
+$database->purgeCachedCollection(
collection: 'users'
);
```
@@ -836,7 +836,7 @@ $database->deleteDocument(
// Delete a cached document
Note: Cached Documents or Collections are automatically deleted when a document or collection is updated or deleted
-$database->deleteCachedDocument(
+$database->purgeCachedDocument(
collection: 'movies',
id: $document->getId()
);
diff --git a/bin/tasks/index.php b/bin/tasks/index.php
index 66c919704..2ef8c4ef3 100644
--- a/bin/tasks/index.php
+++ b/bin/tasks/index.php
@@ -71,7 +71,7 @@
return;
}
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
Console::info("For query: greaterThan(created, 2010-01-01 05:00:00)', 'equal(genre,travel)");
diff --git a/bin/tasks/load.php b/bin/tasks/load.php
index 9362b100d..4658eadd6 100644
--- a/bin/tasks/load.php
+++ b/bin/tasks/load.php
@@ -56,7 +56,7 @@
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes());
$database = new Database(new MariaDB($pdo), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
// Outline collection schema
@@ -89,7 +89,7 @@
$pdo = $pool->get();
$database = new Database(new MariaDB($pdo), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
// Each coroutine loads 1000 documents
@@ -116,7 +116,7 @@
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes());
$database = new Database(new MySQL($pdo), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
// Outline collection schema
@@ -150,7 +150,7 @@
$pdo = $pool->get();
$database = new Database(new MySQL($pdo), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
// Each coroutine loads 1000 documents
@@ -178,7 +178,7 @@
);
$database = new Database(new Mongo($client), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
// Outline collection schema
@@ -192,7 +192,7 @@
for ($i = 0; $i < $limit / 1000; $i++) {
go(function () use ($client, $faker, $name, $namespace, $cache) {
$database = new Database(new Mongo($client), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
// Each coroutine loads 1000 documents
@@ -227,8 +227,8 @@
function createSchema(Database $database): void
{
- if ($database->exists($database->getDefaultDatabase())) {
- $database->delete($database->getDefaultDatabase());
+ if ($database->exists($database->getDatabase())) {
+ $database->delete($database->getDatabase());
}
$database->create();
diff --git a/bin/tasks/query.php b/bin/tasks/query.php
index ab7115563..2fcd29143 100644
--- a/bin/tasks/query.php
+++ b/bin/tasks/query.php
@@ -45,7 +45,7 @@
);
$database = new Database(new Mongo($client), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
break;
@@ -58,7 +58,7 @@
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPDOAttributes());
$database = new Database(new MariaDB($pdo), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
break;
@@ -71,7 +71,7 @@
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MySQL::getPDOAttributes());
$database = new Database(new MySQL($pdo), $cache);
- $database->setDefaultDatabase($name);
+ $database->setDatabase($name);
$database->setNamespace($namespace);
break;
diff --git a/composer.json b/composer.json
index eda86eba2..a94a99477 100755
--- a/composer.json
+++ b/composer.json
@@ -9,7 +9,10 @@
"psr-4": {"Utopia\\Database\\": "src/Database"}
},
"autoload-dev": {
- "psr-4": {"Utopia\\Tests\\": "tests/Database"}
+ "psr-4": {
+ "Tests\\E2E\\": "tests/e2e",
+ "Tests\\Unit\\": "tests/unit"
+ }
},
"scripts": {
"build": [
diff --git a/composer.lock b/composer.lock
index 8f3d208b3..eae5cf37d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -513,16 +513,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.13.5",
+ "version": "v1.13.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "df105cf8ce7a8f0b8a9425ff45cd281a5448e423"
+ "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/df105cf8ce7a8f0b8a9425ff45cd281a5448e423",
- "reference": "df105cf8ce7a8f0b8a9425ff45cd281a5448e423",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/3e3d2ab01c7d8b484c18e6100ecf53639c744fa7",
+ "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7",
"shasum": ""
},
"require": {
@@ -533,13 +533,13 @@
"php": "^8.1.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.34.1",
- "illuminate/view": "^10.26.2",
- "laravel-zero/framework": "^10.1.2",
+ "friendsofphp/php-cs-fixer": "^3.38.0",
+ "illuminate/view": "^10.30.1",
+ "laravel-zero/framework": "^10.3.0",
"mockery/mockery": "^1.6.6",
"nunomaduro/larastan": "^2.6.4",
"nunomaduro/termwind": "^1.15.1",
- "pestphp/pest": "^2.20.0"
+ "pestphp/pest": "^2.24.2"
},
"bin": [
"builds/pint"
@@ -575,7 +575,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2023-10-26T09:26:10+00:00"
+ "time": "2023-11-07T17:59:57+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -839,16 +839,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.10.40",
+ "version": "1.10.44",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d"
+ "reference": "bf84367c53a23f759513985c54ffe0d0c249825b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93c84b5bf7669920d823631e39904d69b9c7dc5d",
- "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bf84367c53a23f759513985c54ffe0d0c249825b",
+ "reference": "bf84367c53a23f759513985c54ffe0d0c249825b",
"shasum": ""
},
"require": {
@@ -897,7 +897,7 @@
"type": "tidelift"
}
],
- "time": "2023-10-30T14:48:31+00:00"
+ "time": "2023-11-21T16:30:46+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -2608,5 +2608,5 @@
"php": ">=8.0"
},
"platform-dev": [],
- "plugin-api-version": "2.2.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/docker-compose.yml b/docker-compose.yml
index af5a18be8..8b386c771 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,7 @@ services:
tests:
container_name: tests
- image: database-dev
+ image: databases-dev
build:
context: .
networks:
diff --git a/phpunit.xml b/phpunit.xml
index 31b947dd6..ccdaa969e 100755
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -9,8 +9,11 @@
processIsolation="false"
stopOnFailure="false">
-
- ./tests/
+
+ ./tests/unit
+
+
+ ./tests/e2e/Adapter
diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php
index 03bbb789e..764f41df6 100644
--- a/src/Database/Adapter.php
+++ b/src/Database/Adapter.php
@@ -7,15 +7,13 @@
abstract class Adapter
{
- /**
- * @var string
- */
+ protected string $database = '';
+
protected string $namespace = '';
- /**
- * @var string
- */
- protected string $defaultDatabase = '';
+ protected bool $shareTables = false;
+
+ protected ?int $tenant = null;
/**
* @var array
@@ -73,15 +71,11 @@ public function resetDebug(): self
* @param string $namespace
*
* @return bool
- * @throws Exception
+ * @throws DatabaseException
*
*/
public function setNamespace(string $namespace): bool
{
- if (empty($namespace)) {
- throw new DatabaseException('Missing namespace');
- }
-
$this->namespace = $this->filter($namespace);
return true;
@@ -93,15 +87,10 @@ public function setNamespace(string $namespace): bool
* Get namespace of current set scope
*
* @return string
- * @throws DatabaseException
*
*/
public function getNamespace(): string
{
- if (empty($this->namespace)) {
- throw new DatabaseException('Missing namespace');
- }
-
return $this->namespace;
}
@@ -111,18 +100,13 @@ public function getNamespace(): string
* Set database to use for current scope
*
* @param string $name
- * @param bool $reset
*
* @return bool
- * @throws Exception
+ * @throws DatabaseException
*/
- public function setDefaultDatabase(string $name, bool $reset = false): bool
+ public function setDatabase(string $name): bool
{
- if (empty($name) && $reset === false) {
- throw new DatabaseException('Missing database');
- }
-
- $this->defaultDatabase = ($reset) ? '' : $this->filter($name);
+ $this->database = $this->filter($name);
return true;
}
@@ -133,16 +117,72 @@ public function setDefaultDatabase(string $name, bool $reset = false): bool
* Get Database from current scope
*
* @return string
- * @throws Exception
+ * @throws DatabaseException
*
*/
- public function getDefaultDatabase(): string
+ public function getDatabase(): string
{
- if (empty($this->defaultDatabase)) {
- throw new DatabaseException('Missing default database');
+ if (empty($this->database)) {
+ throw new DatabaseException('Missing database. Database must be set before use.');
}
- return $this->defaultDatabase;
+ return $this->database;
+ }
+
+ /**
+ * Set Share Tables.
+ *
+ * Set whether to share tables between tenants
+ *
+ * @param bool $shareTables
+ *
+ * @return bool
+ */
+ public function setShareTables(bool $shareTables): bool
+ {
+ $this->shareTables = $shareTables;
+
+ return true;
+ }
+
+ /**
+ * Get Share Tables.
+ *
+ * Get whether to share tables between tenants
+ *
+ * @return bool
+ */
+ public function getShareTables(): bool
+ {
+ return $this->shareTables;
+ }
+
+ /**
+ * Set Tenant.
+ *
+ * Set tenant to use if tables are shared
+ *
+ * @param ?int $tenant
+ *
+ * @return bool
+ */
+ public function setTenant(?int $tenant): bool
+ {
+ $this->tenant = $tenant;
+
+ return true;
+ }
+
+ /**
+ * Get Tenant.
+ *
+ * Get tenant to use for shared tables
+ *
+ * @return ?int
+ */
+ public function getTenant(): ?int
+ {
+ return $this->tenant;
}
/**
@@ -709,11 +749,11 @@ protected function getAttributeSelections(array $queries): array
*
* @param string $value
* @return string
- * @throws Exception
+ * @throws DatabaseException
*/
public function filter(string $value): string
{
- $value = preg_replace("/[^A-Za-z0-9\_\-]/", '', $value);
+ $value = \preg_replace("/[^A-Za-z0-9\_\-]/", '', $value);
if (\is_null($value)) {
throw new DatabaseException('Failed to filter key');
diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php
index 4ee36bafe..35e6b2103 100644
--- a/src/Database/Adapter/MariaDB.php
+++ b/src/Database/Adapter/MariaDB.php
@@ -73,8 +73,6 @@ public function delete(string $name): bool
*/
public function createCollection(string $name, array $attributes = [], array $indexes = []): bool
{
- $database = $this->getDefaultDatabase();
- $namespace = $this->getNamespace();
$id = $this->filter($name);
/** @var array $attributeStrings */
@@ -116,21 +114,35 @@ public function createCollection(string $name, array $attributes = [], array $in
}
$sql = "
- CREATE TABLE IF NOT EXISTS `{$database}`.`{$namespace}_{$id}` (
- `_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `_uid` VARCHAR(255) NOT NULL,
- `_createdAt` datetime(3) DEFAULT NULL,
- `_updatedAt` datetime(3) DEFAULT NULL,
- `_permissions` MEDIUMTEXT DEFAULT NULL,
+ CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id)} (
+ _id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ _uid VARCHAR(255) NOT NULL,
+ _tenant INT(11) UNSIGNED DEFAULT NULL,
+ _createdAt DATETIME(3) DEFAULT NULL,
+ _updatedAt DATETIME(3) DEFAULT NULL,
+ _permissions MEDIUMTEXT DEFAULT NULL,
+ PRIMARY KEY (_id),
" . \implode(' ', $attributeStrings) . "
- PRIMARY KEY (`_id`),
" . \implode(' ', $indexStrings) . "
- UNIQUE KEY `_uid` (`_uid`),
- KEY `_created_at` (`_createdAt`),
- KEY `_updated_at` (`_updatedAt`)
- )
";
+ if ($this->shareTables) {
+ $sql .= "
+ UNIQUE KEY _uid_tenant (_uid, _tenant),
+ KEY _created_at (_tenant, _createdAt),
+ KEY _updated_at (_tenant, _updatedAt),
+ KEY _tenant_id (_tenant, _id)
+ ";
+ } else {
+ $sql .= "
+ UNIQUE KEY _uid (_uid),
+ KEY _created_at (_createdAt),
+ KEY _updated_at (_updatedAt)
+ ";
+ }
+
+ $sql .= ")";
+
$sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql);
try {
@@ -139,17 +151,29 @@ public function createCollection(string $name, array $attributes = [], array $in
->execute();
$sql = "
- CREATE TABLE IF NOT EXISTS `{$database}`.`{$namespace}_{$id}_perms` (
- `_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `_type` VARCHAR(12) NOT NULL,
- `_permission` VARCHAR(255) NOT NULL,
- `_document` VARCHAR(255) NOT NULL,
- PRIMARY KEY (`_id`),
- UNIQUE INDEX `_index1` (`_document`,`_type`,`_permission`),
- INDEX `_permission` (`_permission`,`_type`,`_document`)
- )
+ CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id . '_perms')} (
+ _id int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ _tenant INT(11) UNSIGNED DEFAULT NULL,
+ _type VARCHAR(12) NOT NULL,
+ _permission VARCHAR(255) NOT NULL,
+ _document VARCHAR(255) NOT NULL,
+ PRIMARY KEY (_id),
";
+ if ($this->shareTables) {
+ $sql .= "
+ UNIQUE INDEX _index1 (_document, _tenant, _type, _permission),
+ INDEX _permission (_tenant, _permission, _type)
+ ";
+ } else {
+ $sql .= "
+ UNIQUE INDEX _index1 (_document, _type, _permission),
+ INDEX _permission (_permission, _type)
+ ";
+ }
+
+ $sql .= ")";
+
$sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql);
$this->getPDO()
@@ -166,7 +190,8 @@ public function createCollection(string $name, array $attributes = [], array $in
}
/**
- * Get Collection Size
+ * Get collection size
+ *
* @param string $collection
* @return int
* @throws DatabaseException
@@ -175,7 +200,7 @@ public function getSizeOfCollection(string $collection): int
{
$collection = $this->filter($collection);
$collection = $this->getNamespace() . '_' . $collection;
- $database = $this->getDefaultDatabase();
+ $database = $this->getDatabase();
$name = $database . '/' . $collection;
$permissions = $database . '/' . $collection . '_perms';
@@ -206,7 +231,8 @@ public function getSizeOfCollection(string $collection): int
}
/**
- * Delete Collection
+ * Delete collection
+ *
* @param string $id
* @return bool
* @throws Exception
@@ -345,7 +371,7 @@ public function renameAttribute(string $collection, string $old, string $new): b
* @param bool $twoWay
* @param string $twoWayKey
* @return bool
- * @throws Exception
+ * @throws DatabaseException
*/
public function createRelationship(
string $collection,
@@ -475,6 +501,17 @@ public function updateRelationship(
->execute();
}
+ /**
+ * @param string $collection
+ * @param string $relatedCollection
+ * @param string $type
+ * @param bool $twoWay
+ * @param string $key
+ * @param string $twoWayKey
+ * @param string $side
+ * @return bool
+ * @throws DatabaseException
+ */
public function deleteRelationship(
string $collection,
string $relatedCollection,
@@ -651,9 +688,10 @@ public function deleteIndex(string $collection, string $id): bool
public function createDocument(string $collection, Document $document): Document
{
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
- $attributes['_permissions'] = json_encode($document->getPermissions());
+ $attributes['_permissions'] = \json_encode($document->getPermissions());
$name = $this->filter($collection);
$columns = '';
@@ -663,7 +701,7 @@ public function createDocument(string $collection, Document $document): Document
* Insert Attributes
*/
$bindIndex = 0;
- foreach ($attributes as $attribute => $value) { // Parse statement
+ foreach ($attributes as $attribute => $value) {
$column = $this->filter($attribute);
$bindKey = 'key_' . $bindIndex;
$columns .= "`{$column}`, ";
@@ -671,7 +709,7 @@ public function createDocument(string $collection, Document $document): Document
$bindIndex++;
}
- // Insert manual id if set
+ // Insert internal ID if set
if (!empty($document->getInternalId())) {
$bindKey = '_id';
$columns .= "_id, ";
@@ -679,7 +717,7 @@ public function createDocument(string $collection, Document $document): Document
}
$sql = "
- INSERT INTO {$this->getSQLTable($name)}({$columns} _uid)
+ INSERT INTO {$this->getSQLTable($name)} ({$columns} _uid)
VALUES ({$columnNames} :_uid)
";
@@ -687,21 +725,19 @@ public function createDocument(string $collection, Document $document): Document
$stmt = $this->getPDO()->prepare($sql);
- $stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
+ $stmt->bindValue(':_uid', $document->getId());
- // Bind manual internal id if set
if (!empty($document->getInternalId())) {
- $stmt->bindValue(':_id', $document->getInternalId(), PDO::PARAM_STR);
+ $stmt->bindValue(':_id', $document->getInternalId());
}
$attributeIndex = 0;
- foreach ($attributes as $attribute => $value) {
- if (is_array($value)) { // arrays & objects should be saved as strings
+ foreach ($attributes as $value) {
+ if (is_array($value)) {
$value = json_encode($value);
}
$bindKey = 'key_' . $attributeIndex;
- $attribute = $this->filter($attribute);
$value = (is_bool($value)) ? (int)$value : $value;
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
$attributeIndex++;
@@ -711,19 +747,20 @@ public function createDocument(string $collection, Document $document): Document
foreach (Database::PERMISSIONS as $type) {
foreach ($document->getPermissionsByType($type) as $permission) {
$permission = \str_replace('"', '', $permission);
- $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')";
+ $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}', :_tenant)";
}
}
if (!empty($permissions)) {
- $strPermissions = \implode(', ', $permissions);
+ $permissions = \implode(', ', $permissions);
$sqlPermissions = "
- INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document)
- VALUES {$strPermissions}
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document, _tenant)
+ VALUES {$permissions}
";
$sqlPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sqlPermissions);
$stmtPermissions = $this->getPDO()->prepare($sqlPermissions);
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
}
try {
@@ -787,6 +824,7 @@ public function createDocuments(string $collection, array $documents, int $batch
foreach ($batch as $document) {
$attributes = $document->getAttributes();
$attributes['_uid'] = $document->getId();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = \json_encode($document->getPermissions());
@@ -815,7 +853,7 @@ public function createDocuments(string $collection, array $documents, int $batch
foreach (Database::PERMISSIONS as $type) {
foreach ($document->getPermissionsByType($type) as $permission) {
$permission = \str_replace('"', '', $permission);
- $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')";
+ $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}', :_tenant)";
}
}
}
@@ -835,9 +873,10 @@ public function createDocuments(string $collection, array $documents, int $batch
if (!empty($permissions)) {
$stmtPermissions = $this->getPDO()->prepare(
"
- INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document)
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document, _tenant)
VALUES " . \implode(', ', $permissions)
);
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
$stmtPermissions?->execute();
}
}
@@ -874,6 +913,7 @@ public function createDocuments(string $collection, array $documents, int $batch
public function updateDocument(string $collection, Document $document): Document
{
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = json_encode($document->getPermissions());
@@ -883,10 +923,14 @@ public function updateDocument(string $collection, Document $document): Document
$sql = "
SELECT _type, _permission
- FROM {$this->getSQLTable($name . '_perms')} p
- WHERE p._document = :_uid
+ FROM {$this->getSQLTable($name . '_perms')}
+ WHERE _document = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql);
/**
@@ -894,6 +938,11 @@ public function updateDocument(string $collection, Document $document): Document
*/
$sqlPermissions = $this->getPDO()->prepare($sql);
$sqlPermissions->bindValue(':_uid', $document->getId());
+
+ if ($this->shareTables) {
+ $sqlPermissions->bindValue(':_tenant', $this->tenant);
+ }
+
$sqlPermissions->execute();
$permissions = $sqlPermissions->fetchAll();
$sqlPermissions->closeCursor();
@@ -949,19 +998,27 @@ public function updateDocument(string $collection, Document $document): Document
}
if (!empty($removeQuery)) {
$removeQuery .= ')';
- $removeQuery = "
+ $sql = "
DELETE
FROM {$this->getSQLTable($name . '_perms')}
- WHERE
- _document = :_uid
- {$removeQuery}
+ WHERE _document = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
+ $removeQuery = $sql . $removeQuery;
+
$removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery);
$stmtRemovePermissions = $this->getPDO()->prepare($removeQuery);
$stmtRemovePermissions->bindValue(':_uid', $document->getId());
+ if ($this->shareTables) {
+ $stmtRemovePermissions->bindValue(':_tenant', $this->tenant);
+ }
+
foreach ($removals as $type => $permissions) {
foreach ($permissions as $i => $permission) {
$stmtRemovePermissions->bindValue(":_remove_{$type}_{$i}", $permission);
@@ -976,20 +1033,20 @@ public function updateDocument(string $collection, Document $document): Document
$values = [];
foreach ($additions as $type => $permissions) {
foreach ($permissions as $i => $_) {
- $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i} )";
+ $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i}, :_tenant)";
}
}
$sql = "
- INSERT INTO {$this->getSQLTable($name . '_perms')}
- (_document, _type, _permission) VALUES " . \implode(', ', $values)
- ;
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission, _tenant)
+ VALUES " . \implode(', ', $values);
$sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql);
$stmtAddPermissions = $this->getPDO()->prepare($sql);
$stmtAddPermissions->bindValue(":_uid", $document->getId());
+ $stmtAddPermissions->bindValue(":_tenant", $this->tenant);
foreach ($additions as $type => $permissions) {
foreach ($permissions as $i => $permission) {
$stmtAddPermissions->bindValue(":_add_{$type}_{$i}", $permission);
@@ -1000,7 +1057,6 @@ public function updateDocument(string $collection, Document $document): Document
/**
* Update Attributes
*/
-
$bindIndex = 0;
foreach ($attributes as $attribute => $value) {
$column = $this->filter($attribute);
@@ -1015,20 +1071,27 @@ public function updateDocument(string $collection, Document $document): Document
WHERE _uid = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql);
$stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $document->getId());
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$attributeIndex = 0;
foreach ($attributes as $attribute => $value) {
- if (is_array($value)) { // arrays & objects should be saved as strings
+ if (is_array($value)) {
$value = json_encode($value);
}
$bindKey = 'key_' . $attributeIndex;
- $attribute = $this->filter($attribute);
$value = (is_bool($value)) ? (int)$value : $value;
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
$attributeIndex++;
@@ -1102,6 +1165,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($batch as $index => $document) {
$attributes = $document->getAttributes();
$attributes['_uid'] = $document->getId();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = json_encode($document->getPermissions());
@@ -1126,14 +1190,27 @@ public function updateDocuments(string $collection, array $documents, int $batch
$batchKeys[] = '(' . implode(', ', $bindKeys) . ')';
// Permissions logic
- $permissionsStmt = $this->getPDO()->prepare("
+ $sql = "
SELECT _type, _permission
FROM {$this->getSQLTable($name . '_perms')}
WHERE _document = :_uid
- ");
+ ";
+
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
+ $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql);
+
+ $permissionsStmt = $this->getPDO()->prepare($sql);
$permissionsStmt->bindValue(':_uid', $document->getId());
+
+ if ($this->shareTables) {
+ $permissionsStmt->bindValue(':_tenant', $this->tenant);
+ }
+
$permissionsStmt->execute();
- $permissions = $permissionsStmt->fetchAll(PDO::FETCH_ASSOC);
+ $permissions = $permissionsStmt->fetchAll();
$initial = [];
foreach (Database::PERMISSIONS as $type) {
@@ -1161,10 +1238,16 @@ public function updateDocuments(string $collection, array $documents, int $batch
$removeBindKeys[] = ':uid_' . $index;
$removeBindValues[$bindKey] = $document->getId();
+ $tenantQuery = '';
+ if ($this->shareTables) {
+ $tenantQuery = ' AND _tenant = :_tenant';
+ }
+
$removeQuery .= "(
_document = :uid_{$index}
+ {$tenantQuery}
AND _type = '{$type}'
- AND _permission IN (" . implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) {
+ AND _permission IN (" . \implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) {
$bindKey = 'remove_' . $type . '_' . $index . '_' . $i;
$removeBindKeys[] = ':' . $bindKey;
$removeBindValues[$bindKey] = $permissionsToRemove[$i];
@@ -1203,7 +1286,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
$bindKey = 'add_' . $type . '_' . $index . '_' . $i;
$addBindValues[$bindKey] = $permission;
- $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey})";
+ $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey}, :_tenant)";
if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) {
$addQuery .= ', ';
@@ -1247,20 +1330,22 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($removeBindValues as $key => $value) {
$stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value));
}
-
+ if ($this->shareTables) {
+ $stmtRemovePermissions->bindValue(':_tenant', $this->tenant);
+ }
$stmtRemovePermissions->execute();
}
if (!empty($addQuery)) {
$stmtAddPermissions = $this->getPDO()->prepare("
- INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission`)
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission`, `_tenant`)
VALUES {$addQuery}
");
foreach ($addBindValues as $key => $value) {
$stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value));
}
-
+ $stmtAddPermissions->bindValue(':_tenant', $this->tenant);
$stmtAddPermissions->execute();
}
}
@@ -1306,18 +1391,25 @@ public function increaseDocumentAttribute(string $collection, string $id, string
$sql = "
UPDATE {$this->getSQLTable($name)}
SET `{$attribute}` = `{$attribute}` + :val
- WHERE
- _uid = :_uid
- {$sqlMax}
- {$sqlMin}
+ WHERE _uid = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
+ $sql .= $sqlMax . $sqlMin;
+
$sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql);
$stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $id);
$stmt->bindValue(':val', $value);
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$stmt->execute() || throw new DatabaseException('Failed to update attribute');
return true;
}
@@ -1340,22 +1432,38 @@ public function deleteDocument(string $collection, string $id): bool
WHERE _uid = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql);
$stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $id);
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$sql = "
DELETE FROM {$this->getSQLTable($name . '_perms')}
WHERE _document = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql);
$stmtPermissions = $this->getPDO()->prepare($sql);
$stmtPermissions->bindValue(':_uid', $id);
+ if ($this->shareTables) {
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
+ }
+
try {
$this->getPDO()->beginTransaction();
@@ -1363,7 +1471,7 @@ public function deleteDocument(string $collection, string $id): bool
throw new DatabaseException('Failed to delete document');
}
if (!$stmtPermissions->execute()) {
- throw new DatabaseException('Failed to clean permissions');
+ throw new DatabaseException('Failed to delete permissions');
}
$this->getPDO()->commit();
@@ -1400,6 +1508,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$orderAttributes = \array_map(fn ($orderAttribute) => match ($orderAttribute) {
'$id' => '_uid',
'$internalId' => '_id',
+ '$tenant' => '_tenant',
'$createdAt' => '_createdAt',
'$updatedAt' => '_updatedAt',
default => $orderAttribute
@@ -1483,6 +1592,10 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$where[] = $this->getSQLPermissionsCondition($name, $roles);
}
+ if ($this->shareTables) {
+ $where[] = "table_main._tenant = :_tenant";
+ }
+
$sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$sqlOrder = 'ORDER BY ' . implode(', ', $orders);
$sqlLimit = \is_null($limit) ? '' : 'LIMIT :limit';
@@ -1505,6 +1618,9 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
}
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
if (!empty($cursor) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
$attribute = $orderAttributes[0];
@@ -1512,6 +1628,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$attribute = match ($attribute) {
'_uid' => '$id',
'_id' => '$internalId',
+ '_tenant' => '$tenant',
'_createdAt' => '$createdAt',
'_updatedAt' => '$updatedAt',
default => $attribute
@@ -1548,6 +1665,10 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$results[$index]['$internalId'] = $document['_id'];
unset($results[$index]['_id']);
}
+ if (\array_key_exists('_tenant', $document)) {
+ $results[$index]['$tenant'] = $document['_tenant'];
+ unset($results[$index]['_tenant']);
+ }
if (\array_key_exists('_createdAt', $document)) {
$results[$index]['$createdAt'] = $document['_createdAt'];
unset($results[$index]['_createdAt']);
@@ -1596,6 +1717,10 @@ public function count(string $collection, array $queries = [], ?int $max = null)
$where[] = $this->getSQLPermissionsCondition($name, $roles);
}
+ if ($this->shareTables) {
+ $where[] = "table_main._tenant = :_tenant";
+ }
+
$sqlWhere = !empty($where)
? 'WHERE ' . \implode(' AND ', $where)
: '';
@@ -1604,7 +1729,7 @@ public function count(string $collection, array $queries = [], ?int $max = null)
SELECT COUNT(1) as sum FROM (
SELECT 1
FROM {$this->getSQLTable($name)} table_main
- " . $sqlWhere . "
+ {$sqlWhere}
{$limit}
) table_count
";
@@ -1617,6 +1742,10 @@ public function count(string $collection, array $queries = [], ?int $max = null)
$this->bindConditionValue($stmt, $query);
}
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
if (!\is_null($max)) {
$stmt->bindValue(':max', $max, PDO::PARAM_INT);
}
@@ -1658,6 +1787,10 @@ public function sum(string $collection, string $attribute, array $queries = [],
$where[] = $this->getSQLPermissionsCondition($name, $roles);
}
+ if ($this->shareTables) {
+ $where[] = "table_main._tenant = :_tenant";
+ }
+
$sqlWhere = !empty($where)
? 'WHERE ' . \implode(' AND ', $where)
: '';
@@ -1679,6 +1812,10 @@ public function sum(string $collection, string $attribute, array $queries = [],
$this->bindConditionValue($stmt, $query);
}
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
if (!\is_null($max)) {
$stmt->bindValue(':max', $max, PDO::PARAM_INT);
}
@@ -1755,6 +1892,7 @@ protected function getSQLCondition(Query $query): string
$query->setAttribute(match ($query->getAttribute()) {
'$id' => '_uid',
'$internalId' => '_id',
+ '$tenant' => '_tenant',
'$createdAt' => '_createdAt',
'$updatedAt' => '_updatedAt',
default => $query->getAttribute()
@@ -1851,7 +1989,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true): str
*/
protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string
{
- $type = match ($type) {
+ $sqlType = match ($type) {
Database::INDEX_KEY,
Database::INDEX_ARRAY => 'INDEX',
Database::INDEX_UNIQUE => 'UNIQUE INDEX',
@@ -1859,7 +1997,14 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr
default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT),
};
- return "CREATE {$type} `{$id}` ON {$this->getSQLTable($collection)} ( " . implode(', ', $attributes) . " )";
+ $attributes = \implode(', ', $attributes);
+
+ if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) {
+ // Add tenant as first index column for best performance
+ $attributes = "_tenant, {$attributes}";
+ }
+
+ return "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection)} ({$attributes})";
}
/**
diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php
index 451913461..2e7ba36b4 100644
--- a/src/Database/Adapter/Mongo.php
+++ b/src/Database/Adapter/Mongo.php
@@ -168,21 +168,18 @@ public function createCollection(string $name, array $attributes = [], array $in
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
- $indexesCreated = $this->client->createIndexes($id, [
- [
- 'key' => ['_uid' => $this->getOrder(Database::ORDER_DESC)],
- 'name' => '_uid',
- 'unique' => true,
- 'collation' => [ // https://docs.mongodb.com/manual/core/index-case-insensitive/#create-a-case-insensitive-index
- 'locale' => 'en',
- 'strength' => 1,
- ]
- ],
- [
- 'key' => ['_permissions' => $this->getOrder(Database::ORDER_DESC)],
- 'name' => '_permissions',
+ $indexesCreated = $this->client->createIndexes($id, [[
+ 'key' => ['_uid' => $this->getOrder(Database::ORDER_DESC)],
+ 'name' => '_uid',
+ 'unique' => true,
+ 'collation' => [ // https://docs.mongodb.com/manual/core/index-case-insensitive/#create-a-case-insensitive-index
+ 'locale' => 'en',
+ 'strength' => 1,
]
- ]);
+ ], [
+ 'key' => ['_permissions' => $this->getOrder(Database::ORDER_DESC)],
+ 'name' => '_permissions',
+ ]]);
if (!$indexesCreated) {
return false;
@@ -612,8 +609,7 @@ public function deleteIndex(string $collection, string $id): bool
{
$name = $this->getNamespace() . '_' . $this->filter($collection);
$id = $this->filter($id);
- $collection = $this->getDatabase();
- $collection->dropIndexes($name, [$id]);
+ $this->getClient()->dropIndexes($name, [$id]);
return true;
}
@@ -632,6 +628,11 @@ public function getDocument(string $collection, string $id, array $queries = [])
$name = $this->getNamespace() . '_' . $this->filter($collection);
$filters = ['_uid' => $id];
+
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
+
$options = [];
$selections = $this->getAttributeSelections($queries);
@@ -665,7 +666,10 @@ public function createDocument(string $collection, Document $document): Document
{
$name = $this->getNamespace() . '_' . $this->filter($collection);
$internalId = $document->getInternalId();
- $document->removeAttribute('$internalId');
+
+ $document
+ ->removeAttribute('$internalId')
+ ->setAttribute('$tenant', (string)$this->getTenant());
$record = $this->replaceChars('$', '_', (array)$document);
$record = $this->timeToMongo($record);
@@ -699,7 +703,9 @@ public function createDocuments(string $collection, array $documents, int $batch
$records = [];
foreach ($documents as $document) {
- $document->removeAttribute('$internalId');
+ $document
+ ->removeAttribute('$internalId')
+ ->setAttribute('$tenant', (string)$this->getTenant());
$record = $this->replaceChars('$', '_', (array)$document);
$record = $this->timeToMongo($record);
@@ -732,9 +738,15 @@ private function insertDocument(string $name, array $document): array
try {
$this->client->insert($name, $document);
+ $filters = [];
+ $filters['_uid'] = $document['_uid'];
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
+
$result = $this->client->find(
$name,
- ['_uid' => $document['_uid']],
+ $filters,
['limit' => 1]
)->cursor->firstBatch[0];
@@ -761,8 +773,14 @@ public function updateDocument(string $collection, Document $document): Document
$record = $this->replaceChars('$', '_', $record);
$record = $this->timeToMongo($record);
+ $filters = [];
+ $filters['_uid'] = $document->getId();
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
+
try {
- $this->client->update($name, ['_uid' => $document->getId()], $record);
+ $this->client->update($name, $filters, $record);
} catch (MongoException $e) {
throw new Duplicate($e->getMessage());
}
@@ -790,7 +808,13 @@ public function updateDocuments(string $collection, array $documents, int $batch
$document = $this->replaceChars('$', '_', $document);
$document = $this->timeToMongo($document);
- $this->client->update($name, ['_uid' => $document['_uid']], $document);
+ $filters = [];
+ $filters['_uid'] = $document['_uid'];
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
+
+ $this->client->update($name, $filters, $document);
$documents[$index] = new Document($document);
}
@@ -813,19 +837,23 @@ public function updateDocuments(string $collection, array $documents, int $batch
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, int|float|null $min = null, int|float|null $max = null): bool
{
$attribute = $this->filter($attribute);
- $where = ['_uid' => $id];
+ $filters = ['_uid' => $id];
+
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
if ($max) {
- $where[$attribute] = ['$lte' => $max];
+ $filters[$attribute] = ['$lte' => $max];
}
if ($min) {
- $where[$attribute] = ['$gte' => $min];
+ $filters[$attribute] = ['$gte' => $min];
}
$this->client->update(
$this->getNamespace() . '_' . $this->filter($collection),
- $where,
+ $filters,
['$inc' => [$attribute => $value]],
);
@@ -845,7 +873,13 @@ public function deleteDocument(string $collection, string $id): bool
{
$name = $this->getNamespace() . '_' . $this->filter($collection);
- $result = $this->client->delete($name, ['_uid' => $id]);
+ $filters = [];
+ $filters['_uid'] = $id;
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
+
+ $result = $this->client->delete($name, $filters);
return (!!$result);
}
@@ -890,6 +924,10 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$filters = $this->buildFilters($queries);
+ if ($this->shareTables) {
+ $filters['_tenant'] = (string)$this->getTenant();
+ }
+
// permissions
if (Authorization::$status) { // skip if authorization is disabled
$roles = \implode('|', Authorization::getRoles());
@@ -1219,17 +1257,6 @@ public function sum(string $collection, string $attribute, array $queries = [],
return $this->client->aggregate($name, $pipeline)->cursor->firstBatch[0]->total ?? 0;
}
- /**
- * @param string|null $name
- * @return Client
- *
- * @throws Exception
- */
- protected function getDatabase(string $name = null): Client
- {
- return $this->getClient()->selectDatabase();
- }
-
/**
* @return Client
*
@@ -1269,27 +1296,29 @@ protected function replaceChars(string $from, string $to, array $array): array
if ($from === '_') {
if (array_key_exists('_id', $array)) {
$result['$internalId'] = (string)$array['_id'];
-
unset($result['_id']);
}
-
if (array_key_exists('_uid', $array)) {
$result['$id'] = $array['_uid'];
-
unset($result['_uid']);
}
+ if (array_key_exists('_tenant', $array)) {
+ $result['$tenant'] = $array['_tenant'];
+ unset($result['_tenant']);
+ }
} elseif ($from === '$') {
if (array_key_exists('$id', $array)) {
$result['_uid'] = $array['$id'];
-
unset($result['$id']);
}
-
if (array_key_exists('$internalId', $array)) {
$result['_id'] = new ObjectId($array['$internalId']);
-
unset($result['$internalId']);
}
+ if (array_key_exists('$tenant', $array)) {
+ $result['_tenant'] = $array['$tenant'];
+ unset($result['$tenant']);
+ }
}
return $result;
@@ -1430,9 +1459,14 @@ protected function getAttributeProjection(array $selections, string $prefix = ''
{
$projection = [];
+ $internalKeys = \array_map(
+ fn ($attr) => $attr['$id'],
+ Database::INTERNAL_ATTRIBUTES
+ );
+
foreach ($selections as $selection) {
// Skip internal attributes since all are selected by default
- if (\in_array($selection, Database::INTERNAL_ATTRIBUTES)) {
+ if (\in_array($selection, $internalKeys)) {
continue;
}
@@ -1599,7 +1633,7 @@ public function getCountOfIndexes(Document $collection): int
*/
public static function getCountOfDefaultAttributes(): int
{
- return 6;
+ return \count(Database::INTERNAL_ATTRIBUTES);
}
/**
@@ -1609,7 +1643,7 @@ public static function getCountOfDefaultAttributes(): int
*/
public static function getCountOfDefaultIndexes(): int
{
- return 5;
+ return \count(Database::INTERNAL_INDEXES);
}
/**
@@ -1647,58 +1681,6 @@ public function getSupportForCasting(): bool
return true;
}
- /**
- * Return set namespace.
- *
- * @return string
- * @throws Exception
- */
- public function getNamespace(): string
- {
- if (empty($this->namespace)) {
- throw new DatabaseException('Missing namespace');
- }
-
- return $this->namespace;
- }
-
- /**
- * Set's default database.
- *
- * @param string $name
- * @param bool $reset
- * @return bool
- * @throws Exception
- */
- public function setDefaultDatabase(string $name, bool $reset = false): bool
- {
- if (empty($name) && $reset === false) {
- throw new DatabaseException('Missing database');
- }
-
- $this->defaultDatabase = ($reset) ? '' : $this->filter($name);
-
- return true;
- }
-
- /**
- * Set's the namespace.
- *
- * @param string $namespace
- * @return bool
- * @throws Exception
- */
- public function setNamespace(string $namespace): bool
- {
- if (empty($namespace)) {
- throw new DatabaseException('Missing namespace');
- }
-
- $this->namespace = $this->filter($namespace);
-
- return true;
- }
-
/**
* Flattens the array.
*
@@ -1773,6 +1755,7 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL
if (!$this->getSupportForTimeouts()) {
return;
}
+
$this->timeout = $milliseconds;
}
diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php
index da6bf6dd7..23d98cadc 100644
--- a/src/Database/Adapter/MySQL.php
+++ b/src/Database/Adapter/MySQL.php
@@ -47,7 +47,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr
throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT);
}
- return 'CREATE '.$type.' `'.$id.'` ON `'.$this->getDefaultDatabase().'`.`'.$this->getNamespace().'_'.$collection.'` ( '.implode(', ', $attributes).' );';
+ return 'CREATE '.$type.' `'.$id.'` ON `'.$this->getDatabase().'`.`'.$this->getNamespace().'_'.$collection.'` ( '.implode(', ', $attributes).' );';
}
/**
@@ -103,7 +103,7 @@ public function getSizeOfCollection(string $collection): int
{
$collection = $this->filter($collection);
$collection = $this->getNamespace() . '_' . $collection;
- $database = $this->getDefaultDatabase();
+ $database = $this->getDatabase();
$name = $database . '/' . $collection;
$permissions = $database . '/' . $collection . '_perms';
diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php
index d065b9850..b92a780fb 100644
--- a/src/Database/Adapter/Postgres.php
+++ b/src/Database/Adapter/Postgres.php
@@ -31,6 +31,7 @@ class Postgres extends SQL
* @param string $name
*
* @return bool
+ * @throws DatabaseException
*/
public function create(string $name): bool
{
@@ -78,7 +79,6 @@ public function delete(string $name): bool
*/
public function createCollection(string $name, array $attributes = [], array $indexes = []): bool
{
- $database = $this->getDefaultDatabase();
$namespace = $this->getNamespace();
$id = $this->filter($name);
@@ -100,20 +100,32 @@ public function createCollection(string $name, array $attributes = [], array $in
*/
$sql = "
CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id)} (
- \"_id\" SERIAL NOT NULL,
- \"_uid\" VARCHAR(255) NOT NULL,
+ _id SERIAL NOT NULL,
+ _uid VARCHAR(255) NOT NULL,
+ _tenant INTEGER DEFAULT NULL,
\"_createdAt\" TIMESTAMP(3) DEFAULT NULL,
\"_updatedAt\" TIMESTAMP(3) DEFAULT NULL,
- \"_permissions\" TEXT DEFAULT NULL,
+ _permissions TEXT DEFAULT NULL,
" . \implode(' ', $attributes) . "
- PRIMARY KEY (\"_id\")
+ PRIMARY KEY (_id)
);
-
- CREATE UNIQUE INDEX \"{$namespace}_{$id}_uid\" on {$this->getSQLTable($id)} (LOWER(\"_uid\"));
- CREATE INDEX \"{$namespace}_{$id}_created\" ON {$this->getSQLTable($id)} (\"_createdAt\");
- CREATE INDEX \"{$namespace}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\");
";
+ if ($this->shareTables) {
+ $sql .= "
+ CREATE UNIQUE INDEX \"{$namespace}_{$this->tenant}_{$id}_uid\" ON {$this->getSQLTable($id)} (LOWER(_uid), _tenant);
+ CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_created\" ON {$this->getSQLTable($id)} (_tenant, \"_createdAt\");
+ CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_updated\" ON {$this->getSQLTable($id)} (_tenant, \"_updatedAt\");
+ CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_tenant_id\" ON {$this->getSQLTable($id)} (_tenant, _id);
+ ";
+ } else {
+ $sql .= "
+ CREATE UNIQUE INDEX \"{$namespace}_{$id}_uid\" ON {$this->getSQLTable($id)} (LOWER(_uid));
+ CREATE INDEX \"{$namespace}_{$id}_created\" ON {$this->getSQLTable($id)} (\"_createdAt\");
+ CREATE INDEX \"{$namespace}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\");
+ ";
+ }
+
$sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql);
$stmt = $this->getPDO()->prepare($sql);
@@ -123,18 +135,31 @@ public function createCollection(string $name, array $attributes = [], array $in
$sql = "
CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id . '_perms')} (
- \"_id\" SERIAL NOT NULL,
- \"_type\" VARCHAR(12) NOT NULL,
- \"_permission\" VARCHAR(255) NOT NULL,
- \"_document\" VARCHAR(255) NOT NULL,
- PRIMARY KEY (\"_id\")
- );
- CREATE UNIQUE INDEX \"index_{$namespace}_{$id}_ukey\"
- ON {$this->getSQLTable($id. '_perms')} USING btree (\"_document\",\"_type\",\"_permission\");
- CREATE INDEX \"index_{$namespace}_{$id}_permission\"
- ON {$this->getSQLTable($id. '_perms')} USING btree (\"_permission\",\"_type\",\"_document\");
+ _id SERIAL NOT NULL,
+ _tenant INTEGER DEFAULT NULL,
+ _type VARCHAR(12) NOT NULL,
+ _permission VARCHAR(255) NOT NULL,
+ _document VARCHAR(255) NOT NULL,
+ PRIMARY KEY (_id)
+ );
";
+ if ($this->shareTables) {
+ $sql .= "
+ CREATE UNIQUE INDEX \"{$namespace}_{$this->tenant}_{$id}_ukey\"
+ ON {$this->getSQLTable($id. '_perms')} USING btree (_tenant,_document,_type,_permission);
+ CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_permission\"
+ ON {$this->getSQLTable($id. '_perms')} USING btree (_tenant,_permission,_type);
+ ";
+ } else {
+ $sql .= "
+ CREATE UNIQUE INDEX \"{$namespace}_{$id}_ukey\"
+ ON {$this->getSQLTable($id. '_perms')} USING btree (_document,_type,_permission);
+ CREATE INDEX \"{$namespace}_{$id}_permission\"
+ ON {$this->getSQLTable($id. '_perms')} USING btree (_permission,_type);
+ ";
+ }
+
$sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql);
$this->getPDO()
@@ -608,9 +633,11 @@ public function deleteIndex(string $collection, string $id): bool
{
$name = $this->filter($collection);
$id = $this->filter($id);
- $schemaName = $this->getDefaultDatabase();
+ $schemaName = $this->getDatabase();
- $sql = "DROP INDEX IF EXISTS \"{$schemaName}\".{$id}";
+ $key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
+
+ $sql = "DROP INDEX IF EXISTS \"{$schemaName}\".{$key}";
$sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql);
return $this->getPDO()
@@ -634,8 +661,8 @@ public function renameIndex(string $collection, string $old, string $new): bool
$namespace = $this->getNamespace();
$old = $this->filter($old);
$new = $this->filter($new);
- $oldIndexName = $collection . "_" . $old;
- $newIndexName = $namespace . $collection . "_" . $new;
+ $oldIndexName = "{$this->tenant}_{$collection}_{$old}";
+ $newIndexName = "{$namespace}_{$this->tenant}_{$collection}_{$new}";
$sql = "ALTER INDEX {$this->getSQLTable($oldIndexName)} RENAME TO \"{$newIndexName}\"";
$sql = $this->trigger(Database::EVENT_INDEX_RENAME, $sql);
@@ -656,9 +683,10 @@ public function renameIndex(string $collection, string $old, string $new): bool
public function createDocument(string $collection, Document $document): Document
{
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
- $attributes['_permissions'] = json_encode($document->getPermissions());
+ $attributes['_permissions'] = \json_encode($document->getPermissions());
$name = $this->filter($collection);
$columns = '';
@@ -670,7 +698,7 @@ public function createDocument(string $collection, Document $document): Document
* Insert Attributes
*/
- // Insert manual id if set
+ // Insert internal id if set
if (!empty($document->getInternalId())) {
$bindKey = '_id';
$columns .= "\"_id\", ";
@@ -678,7 +706,7 @@ public function createDocument(string $collection, Document $document): Document
}
$bindIndex = 0;
- foreach ($attributes as $attribute => $value) { // Parse statement
+ foreach ($attributes as $attribute => $value) {
$column = $this->filter($attribute);
$bindKey = 'key_' . $bindIndex;
$columns .= "\"{$column}\", ";
@@ -687,8 +715,9 @@ public function createDocument(string $collection, Document $document): Document
}
$sql = "
- INSERT INTO {$this->getSQLTable($name)} ({$columns}\"_uid\")
- VALUES ({$columnNames}:_uid) RETURNING _id
+ INSERT INTO {$this->getSQLTable($name)} ({$columns} \"_uid\")
+ VALUES ({$columnNames} :_uid)
+ RETURNING _id
";
$sql = $this->trigger(Database::EVENT_DOCUMENT_CREATE, $sql);
@@ -697,19 +726,17 @@ public function createDocument(string $collection, Document $document): Document
$stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
- // Bind manual internal id if set
if (!empty($document->getInternalId())) {
$stmt->bindValue(':_id', $document->getInternalId(), PDO::PARAM_STR);
}
$attributeIndex = 0;
- foreach ($attributes as $attribute => $value) {
- if (is_array($value)) { // arrays & objects should be saved as strings
- $value = json_encode($value);
+ foreach ($attributes as $value) {
+ if (is_array($value)) {
+ $value = \json_encode($value);
}
$bindKey = 'key_' . $attributeIndex;
- $attribute = $this->filter($attribute);
$value = (is_bool($value)) ? ($value ? "true" : "false") : $value;
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
$attributeIndex++;
@@ -719,19 +746,22 @@ public function createDocument(string $collection, Document $document): Document
foreach (Database::PERMISSIONS as $type) {
foreach ($document->getPermissionsByType($type) as $permission) {
$permission = \str_replace('"', '', $permission);
- $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')";
+ $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}', :_tenant)";
}
}
if (!empty($permissions)) {
+ $permissions = \implode(', ', $permissions);
+
$queryPermissions = "
- INSERT INTO {$this->getSQLTable($name . '_perms')}
- (_type, _permission, _document) VALUES " . implode(', ', $permissions);
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document, _tenant)
+ VALUES {$permissions}
+ ";
$queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions);
-
$stmtPermissions = $this->getPDO()->prepare($queryPermissions);
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
}
try {
@@ -747,7 +777,6 @@ public function createDocument(string $collection, Document $document): Document
case 23505:
$this->getPDO()->rollBack();
throw new Duplicate('Duplicated document: ' . $e->getMessage());
-
default:
throw $e;
}
@@ -791,6 +820,7 @@ public function createDocuments(string $collection, array $documents, int $batch
foreach ($batch as $document) {
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_uid'] = $document->getId();
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
@@ -820,7 +850,7 @@ public function createDocuments(string $collection, array $documents, int $batch
foreach (Database::PERMISSIONS as $type) {
foreach ($document->getPermissionsByType($type) as $permission) {
$permission = \str_replace('"', '', $permission);
- $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')";
+ $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}', :_tenant)";
}
}
}
@@ -840,15 +870,16 @@ public function createDocuments(string $collection, array $documents, int $batch
if (!empty($permissions)) {
$stmtPermissions = $this->getPDO()->prepare(
"
- INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document)
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_type, _permission, _document, _tenant)
VALUES " . \implode(', ', $permissions)
);
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
$stmtPermissions?->execute();
}
}
if (!$this->getPDO()->commit()) {
- throw new Exception('Failed to commit transaction');
+ throw new DatabaseException('Failed to commit transaction');
}
return $documents;
@@ -874,6 +905,7 @@ public function createDocuments(string $collection, array $documents, int $batch
public function updateDocument(string $collection, Document $document): Document
{
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = json_encode($document->getPermissions());
@@ -883,10 +915,14 @@ public function updateDocument(string $collection, Document $document): Document
$sql = "
SELECT _type, _permission
- FROM {$this->getSQLTable($name . '_perms')} p
- WHERE p._document = :_uid
+ FROM {$this->getSQLTable($name . '_perms')}
+ WHERE _document = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql);
/**
@@ -894,6 +930,11 @@ public function updateDocument(string $collection, Document $document): Document
*/
$permissionsStmt = $this->getPDO()->prepare($sql);
$permissionsStmt->bindValue(':_uid', $document->getId());
+
+ if ($this->shareTables) {
+ $permissionsStmt->bindValue(':_tenant', $this->tenant);
+ }
+
$permissionsStmt->execute();
$permissions = $permissionsStmt->fetchAll();
$permissionsStmt->closeCursor();
@@ -951,17 +992,27 @@ public function updateDocument(string $collection, Document $document): Document
}
if (!empty($removeQuery)) {
$removeQuery .= ')';
- $removeQuery = "
+
+ $sql = "
DELETE
FROM {$this->getSQLTable($name . '_perms')}
- WHERE
- _document = :_uid
- {$removeQuery}
+ WHERE _document = :_uid
";
+
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
+ $removeQuery = $sql . $removeQuery;
+
$removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery);
$stmtRemovePermissions = $this->getPDO()->prepare($removeQuery);
$stmtRemovePermissions->bindValue(':_uid', $document->getId());
+ if ($this->shareTables) {
+ $stmtRemovePermissions->bindValue(':_tenant', $this->tenant);
+ }
+
foreach ($removals as $type => $permissions) {
foreach ($permissions as $i => $permission) {
$stmtRemovePermissions->bindValue(":_remove_{$type}_{$i}", $permission);
@@ -976,19 +1027,20 @@ public function updateDocument(string $collection, Document $document): Document
$values = [];
foreach ($additions as $type => $permissions) {
foreach ($permissions as $i => $_) {
- $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i} )";
+ $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i}, :_tenant)";
}
}
$sql = "
- INSERT INTO {$this->getSQLTable($name . '_perms')}
- (_document, _type, _permission) VALUES" . \implode(', ', $values);
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission, _tenant)
+ VALUES" . \implode(', ', $values);
$sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql);
$stmtAddPermissions = $this->getPDO()->prepare($sql);
-
$stmtAddPermissions->bindValue(":_uid", $document->getId());
+ $stmtAddPermissions->bindValue(':_tenant', $this->tenant);
+
foreach ($additions as $type => $permissions) {
foreach ($permissions as $i => $permission) {
$stmtAddPermissions->bindValue(":_add_{$type}_{$i}", $permission);
@@ -1010,23 +1062,31 @@ public function updateDocument(string $collection, Document $document): Document
$sql = "
UPDATE {$this->getSQLTable($name)}
- SET {$columns} _uid = :_uid WHERE _uid = :_uid
+ SET {$columns} _uid = :_uid
+ WHERE _uid = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql);
$stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $document->getId());
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$attributeIndex = 0;
foreach ($attributes as $attribute => $value) {
- if (is_array($value)) { // arrays & objects should be saved as strings
+ if (is_array($value)) {
$value = json_encode($value);
}
$bindKey = 'key_' . $attributeIndex;
- $attribute = $this->filter($attribute);
$value = (is_bool($value)) ? ($value == true ? "true" : "false") : $value;
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
$attributeIndex++;
@@ -1096,6 +1156,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($batch as $index => $document) {
$attributes = $document->getAttributes();
$attributes['_uid'] = $document->getId();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = json_encode($document->getPermissions());
@@ -1120,14 +1181,27 @@ public function updateDocuments(string $collection, array $documents, int $batch
$batchKeys[] = '(' . implode(', ', $bindKeys) . ')';
// Permissions logic
- $permissionsStmt = $this->getPDO()->prepare("
+ $sql = "
SELECT _type, _permission
FROM {$this->getSQLTable($name . '_perms')}
WHERE _document = :_uid
- ");
+ ";
+
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
+ $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql);
+
+ $permissionsStmt = $this->getPDO()->prepare($sql);
$permissionsStmt->bindValue(':_uid', $document->getId());
+
+ if ($this->shareTables) {
+ $permissionsStmt->bindValue(':_tenant', $this->tenant);
+ }
+
$permissionsStmt->execute();
- $permissions = $permissionsStmt->fetchAll(PDO::FETCH_ASSOC);
+ $permissions = $permissionsStmt->fetchAll();
$initial = [];
foreach (Database::PERMISSIONS as $type) {
@@ -1155,8 +1229,14 @@ public function updateDocuments(string $collection, array $documents, int $batch
$removeBindKeys[] = ':uid_' . $index;
$removeBindValues[$bindKey] = $document->getId();
+ $tenantQuery = '';
+ if ($this->shareTables) {
+ $tenantQuery = ' AND _tenant = :_tenant';
+ }
+
$removeQuery .= "(
_document = :uid_{$index}
+ {$tenantQuery}
AND _type = '{$type}'
AND _permission IN (" . implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) {
$bindKey = 'remove_' . $type . '_' . $index . '_' . $i;
@@ -1197,7 +1277,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
$bindKey = 'add_' . $type . '_' . $index . '_' . $i;
$addBindValues[$bindKey] = $permission;
- $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey})";
+ $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey}, :_tenant)";
if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) {
$addQuery .= ', ';
@@ -1219,11 +1299,18 @@ public function updateDocuments(string $collection, array $documents, int $batch
$updateClause .= "{$column} = excluded.{$column}";
}
- $stmt = $this->getPDO()->prepare("
+ $sql = "
INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ")
VALUES " . \implode(', ', $batchKeys) . "
- ON CONFLICT (LOWER(_uid)) DO UPDATE SET $updateClause
- ");
+ ";
+
+ if ($this->shareTables) {
+ $sql .= "ON CONFLICT (_tenant, LOWER(_uid)) DO UPDATE SET $updateClause";
+ } else {
+ $sql .= "ON CONFLICT (LOWER(_uid)) DO UPDATE SET $updateClause";
+ }
+
+ $stmt = $this->getPDO()->prepare($sql);
foreach ($bindValues as $key => $value) {
$stmt->bindValue($key, $value, $this->getPDOType($value));
@@ -1241,26 +1328,29 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($removeBindValues as $key => $value) {
$stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value));
}
+ if ($this->shareTables) {
+ $stmtRemovePermissions->bindValue(':_tenant', $this->tenant);
+ }
$stmtRemovePermissions->execute();
}
if (!empty($addQuery)) {
$stmtAddPermissions = $this->getPDO()->prepare("
- INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission)
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission, _tenant)
VALUES {$addQuery}
");
foreach ($addBindValues as $key => $value) {
$stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value));
}
-
+ $stmtAddPermissions->bindValue(':_tenant', $this->tenant);
$stmtAddPermissions->execute();
}
}
if (!$this->getPDO()->commit()) {
- throw new Exception('Failed to commit transaction');
+ throw new DatabaseException('Failed to commit transaction');
}
return $documents;
@@ -1297,18 +1387,25 @@ public function increaseDocumentAttribute(string $collection, string $id, string
$sql = "
UPDATE {$this->getSQLTable($name)}
SET \"{$attribute}\" = \"{$attribute}\" + :val
- WHERE
- _uid = :_uid
- {$sqlMax}
- {$sqlMin}
+ WHERE _uid = :_uid
";
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
+ $sql .= $sqlMax . $sqlMin;
+
$sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql);
$stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $id);
$stmt->bindValue(':val', $value);
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$stmt->execute() || throw new DatabaseException('Failed to update attribute');
return true;
}
@@ -1332,28 +1429,43 @@ public function deleteDocument(string $collection, string $id): bool
WHERE _uid = :_uid
";
- $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql);
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+ $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql);
$stmt = $this->getPDO()->prepare($sql);
-
$stmt->bindValue(':_uid', $id, PDO::PARAM_STR);
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$sql = "
DELETE FROM {$this->getSQLTable($name . '_perms')}
WHERE _document = :_uid
";
+
+ if ($this->shareTables) {
+ $sql .= ' AND _tenant = :_tenant';
+ }
+
$sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql);
$stmtPermissions = $this->getPDO()->prepare($sql);
$stmtPermissions->bindValue(':_uid', $id);
+ if ($this->shareTables) {
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
+ }
+
try {
if (!$stmt->execute()) {
throw new DatabaseException('Failed to delete document');
}
if (!$stmtPermissions->execute()) {
- throw new DatabaseException('Failed to clean permissions');
+ throw new DatabaseException('Failed to delete permissions');
}
} catch (\Throwable $th) {
$this->getPDO()->rollBack();
@@ -1396,6 +1508,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$orderAttributes = \array_map(fn ($orderAttribute) => match ($orderAttribute) {
'$id' => '_uid',
'$internalId' => '_id',
+ '$tenant' => '_tenant',
'$createdAt' => '_createdAt',
'$updatedAt' => '_updatedAt',
default => $orderAttribute
@@ -1468,6 +1581,9 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$where[] = $this->getSQLCondition($query);
}
+ if ($this->shareTables) {
+ $where[] = "table_main._tenant = :_tenant";
+ }
if (Authorization::$status) {
$where[] = $this->getSQLPermissionsCondition($name, $roles);
@@ -1494,6 +1610,9 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
}
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
if (!empty($cursor) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
$attribute = $orderAttributes[0];
@@ -1501,6 +1620,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$attribute = match ($attribute) {
'_uid' => '$id',
'_id' => '$internalId',
+ '_tenant' => '$tenant',
'_createdAt' => '$createdAt',
'_updatedAt' => '$updatedAt',
default => $attribute
@@ -1537,6 +1657,10 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
$results[$index]['$internalId'] = $document['_id'];
unset($results[$index]['_id']);
}
+ if (\array_key_exists('_tenant', $document)) {
+ $results[$index]['$tenant'] = $document['_tenant'];
+ unset($results[$index]['_tenant']);
+ }
if (\array_key_exists('_createdAt', $document)) {
$results[$index]['$createdAt'] = $document['_createdAt'];
unset($results[$index]['_createdAt']);
@@ -1582,6 +1706,10 @@ public function count(string $collection, array $queries = [], ?int $max = null)
$where[] = $this->getSQLCondition($query);
}
+ if ($this->shareTables) {
+ $where[] = "table_main._tenant = :_tenant";
+ }
+
if (Authorization::$status) {
$where[] = $this->getSQLPermissionsCondition($name, $roles);
}
@@ -1591,7 +1719,7 @@ public function count(string $collection, array $queries = [], ?int $max = null)
SELECT COUNT(1) as sum FROM (
SELECT 1
FROM {$this->getSQLTable($name)} table_main
- " . $sqlWhere . "
+ {$sqlWhere}
{$limit}
) table_count
";
@@ -1603,6 +1731,9 @@ public function count(string $collection, array $queries = [], ?int $max = null)
foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
}
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
if (!\is_null($max)) {
$stmt->bindValue(':max', $max, PDO::PARAM_INT);
@@ -1638,6 +1769,10 @@ public function sum(string $collection, string $attribute, array $queries = [],
$where[] = $this->getSQLCondition($query);
}
+ if ($this->shareTables) {
+ $where[] = "table_main._tenant = :_tenant";
+ }
+
if (Authorization::$status) {
$where[] = $this->getSQLPermissionsCondition($name, $roles);
}
@@ -1662,6 +1797,9 @@ public function sum(string $collection, string $attribute, array $queries = [],
foreach ($queries as $query) {
$this->bindConditionValue($stmt, $query);
}
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
if (!\is_null($max)) {
$stmt->bindValue(':max', $max, PDO::PARAM_INT);
@@ -1736,6 +1874,7 @@ protected function getSQLCondition(Query $query): string
$query->setAttribute(match ($query->getAttribute()) {
'$id' => '_uid',
'$internalId' => '_id',
+ '$tenant' => '_tenant',
'$createdAt' => '_createdAt',
'$updatedAt' => '_updatedAt',
default => $query->getAttribute()
@@ -1840,7 +1979,7 @@ protected function getSQLType(string $type, int $size, bool $signed = true): str
*/
protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string
{
- $type = match ($type) {
+ $sqlType = match ($type) {
Database::INDEX_KEY,
Database::INDEX_ARRAY,
Database::INDEX_FULLTEXT => 'INDEX',
@@ -1848,7 +1987,15 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr
default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_ARRAY . ', ' . Database::INDEX_FULLTEXT),
};
- return 'CREATE ' . $type . ' "' . $this->getNamespace() . '_' . $collection . '_' . $id . '" ON ' . $this->getSQLTable($collection) . ' ( ' . implode(', ', $attributes) . ' );';
+ $key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
+ $attributes = \implode(', ', $attributes);
+
+ if ($this->shareTables && $type !== Database::INDEX_FULLTEXT) {
+ // Add tenant as first index column for best performance
+ $attributes = "_tenant, {$attributes}";
+ }
+
+ return "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)} ({$attributes});";
}
/**
@@ -1862,7 +2009,7 @@ protected function getSQLSchema(): string
return '';
}
- return "\"{$this->getDefaultDatabase()}\".";
+ return "\"{$this->getDatabase()}\".";
}
/**
@@ -1873,7 +2020,7 @@ protected function getSQLSchema(): string
*/
protected function getSQLTable(string $name): string
{
- return "\"{$this->getDefaultDatabase()}\".\"{$this->getNamespace()}_{$name}\"";
+ return "\"{$this->getDatabase()}\".\"{$this->getNamespace()}_{$name}\"";
}
/**
diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php
index 344b74b72..4f3904f91 100644
--- a/src/Database/Adapter/SQL.php
+++ b/src/Database/Adapter/SQL.php
@@ -48,7 +48,7 @@ public function ping(): bool
* @param string $database
* @param string|null $collection
* @return bool
- * @throws Exception
+ * @throws DatabaseException
*/
public function exists(string $database, ?string $collection = null): bool
{
@@ -56,11 +56,20 @@ public function exists(string $database, ?string $collection = null): bool
if (!\is_null($collection)) {
$collection = $this->filter($collection);
- $stmt = $this->getPDO()->prepare("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table");
+ $stmt = $this->getPDO()->prepare("
+ SELECT TABLE_NAME
+ FROM INFORMATION_SCHEMA.TABLES
+ WHERE TABLE_SCHEMA = :schema
+ AND TABLE_NAME = :table
+ ");
$stmt->bindValue(':schema', $database, PDO::PARAM_STR);
$stmt->bindValue(':table', "{$this->getNamespace()}_{$collection}", PDO::PARAM_STR);
} else {
- $stmt = $this->getPDO()->prepare("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :schema");
+ $stmt = $this->getPDO()->prepare("
+ SELECT SCHEMA_NAME FROM
+ INFORMATION_SCHEMA.SCHEMATA
+ WHERE SCHEMA_NAME = :schema
+ ");
$stmt->bindValue(':schema', $database, PDO::PARAM_STR);
}
@@ -98,16 +107,26 @@ public function list(): array
public function getDocument(string $collection, string $id, array $queries = []): Document
{
$name = $this->filter($collection);
-
$selections = $this->getAttributeSelections($queries);
- $stmt = $this->getPDO()->prepare("
- SELECT {$this->getAttributeProjection($selections)}
+ $sql = "
+ SELECT {$this->getAttributeProjection($selections)}
FROM {$this->getSQLTable($name)}
- WHERE _uid = :_uid;
- ");
+ WHERE _uid = :_uid
+ ";
+
+ if ($this->shareTables) {
+ $sql .= "AND _tenant = :_tenant";
+ }
+
+ $stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $id);
+
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->getTenant());
+ }
+
$stmt->execute();
$document = $stmt->fetchAll();
@@ -127,6 +146,10 @@ public function getDocument(string $collection, string $id, array $queries = [])
$document['$id'] = $document['_uid'];
unset($document['_uid']);
}
+ if (\array_key_exists('_tenant', $document)) {
+ $document['$tenant'] = $document['_tenant'];
+ unset($document['_tenant']);
+ }
if (\array_key_exists('_createdAt', $document)) {
$document['$createdAt'] = $document['_createdAt'];
unset($document['_createdAt']);
@@ -259,7 +282,7 @@ public function getCountOfIndexes(Document $collection): int
*/
public static function getCountOfDefaultAttributes(): int
{
- return 4;
+ return \count(Database::INTERNAL_ATTRIBUTES);
}
/**
@@ -269,7 +292,7 @@ public static function getCountOfDefaultAttributes(): int
*/
public static function getCountOfDefaultIndexes(): int
{
- return 5;
+ return \count(Database::INTERNAL_INDEXES);
}
/**
@@ -304,34 +327,20 @@ public function getAttributeWidth(Document $collection): int
foreach ($attributes as $attribute) {
switch ($attribute['type']) {
case Database::VAR_STRING:
- switch (true) {
- case ($attribute['size'] > 16777215):
- // 8 bytes length + 4 bytes for LONGTEXT
- $total += 12;
- break;
-
- case ($attribute['size'] > 65535):
- // 8 bytes length + 3 bytes for MEDIUMTEXT
- $total += 11;
- break;
-
- case ($attribute['size'] > $this->getMaxVarcharLength()):
- // 8 bytes length + 2 bytes for TEXT
- $total += 10;
- break;
-
- case ($attribute['size'] > 255):
- // $size = $size * 4; // utf8mb4 up to 4 bytes per char
- // 8 bytes length + 2 bytes for VARCHAR(>255)
- $total += ($attribute['size'] * 4) + 2;
- break;
-
- default:
- // $size = $size * 4; // utf8mb4 up to 4 bytes per char
- // 8 bytes length + 1 bytes for VARCHAR(<=255)
- $total += ($attribute['size'] * 4) + 1;
- break;
- }
+ $total += match (true) {
+ // 8 bytes length + 4 bytes for LONGTEXT
+ $attribute['size'] > 16777215 => 12,
+ // 8 bytes length + 3 bytes for MEDIUMTEXT
+ $attribute['size'] > 65535 => 11,
+ // 8 bytes length + 2 bytes for TEXT
+ $attribute['size'] > $this->getMaxVarcharLength() => 10,
+ // $size = $size * 4; // utf8mb4 up to 4 bytes per char
+ // 8 bytes length + 2 bytes for VARCHAR(>255)
+ $attribute['size'] > 255 => ($attribute['size'] * 4) + 2,
+ // $size = $size * 4; // utf8mb4 up to 4 bytes per char
+ // 8 bytes length + 1 bytes for VARCHAR(<=255)
+ default => ($attribute['size'] * 4) + 1,
+ };
break;
case Database::VAR_INTEGER:
@@ -828,28 +837,21 @@ protected function getSQLIndexType(string $type): string
protected function getSQLPermissionsCondition(string $collection, array $roles): string
{
$roles = array_map(fn (string $role) => $this->getPDO()->quote($role), $roles);
+
+ $tenantQuery = '';
+ if ($this->shareTables) {
+ $tenantQuery = 'AND _tenant = :_tenant';
+ }
+
return "table_main._uid IN (
SELECT _document
FROM {$this->getSQLTable($collection . '_perms')}
WHERE _permission IN (" . implode(', ', $roles) . ")
- AND _type = 'read'
+ AND _type = 'read'
+ {$tenantQuery}
)";
}
- /**
- * Get SQL schema
- *
- * @return string
- */
- protected function getSQLSchema(): string
- {
- if (!$this->getSupportForSchemas()) {
- return '';
- }
-
- return "`{$this->getDefaultDatabase()}`.";
- }
-
/**
* Get SQL table
*
@@ -858,7 +860,7 @@ protected function getSQLSchema(): string
*/
protected function getSQLTable(string $name): string
{
- return "{$this->getSQLSchema()}`{$this->getNamespace()}_{$name}`";
+ return "`{$this->getDatabase()}`.`{$this->getNamespace()}_{$name}`";
}
/**
diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php
index a11c374d5..5e9cffd61 100644
--- a/src/Database/Adapter/SQLite.php
+++ b/src/Database/Adapter/SQLite.php
@@ -120,7 +120,12 @@ public function createCollection(string $name, array $attributes = [], array $in
foreach ($attributes as $key => $attribute) {
$attrId = $this->filter($attribute->getId());
- $attrType = $this->getSQLType($attribute->getAttribute('type'), $attribute->getAttribute('size', 0), $attribute->getAttribute('signed', true));
+
+ $attrType = $this->getSQLType(
+ $attribute->getAttribute('type'),
+ $attribute->getAttribute('size', 0),
+ $attribute->getAttribute('signed', true)
+ );
if ($attribute->getAttribute('array')) {
$attrType = 'LONGTEXT';
@@ -130,11 +135,12 @@ public function createCollection(string $name, array $attributes = [], array $in
}
$sql = "
- CREATE TABLE IF NOT EXISTS `{$namespace}_{$id}` (
+ CREATE TABLE IF NOT EXISTS `{$this->getSQLTable($id)}` (
`_id` INTEGER PRIMARY KEY AUTOINCREMENT,
- `_uid` CHAR(255) NOT NULL,
- `_createdAt` datetime(3) DEFAULT NULL,
- `_updatedAt` datetime(3) DEFAULT NULL,
+ `_uid` VARCHAR(36) NOT NULL,
+ `_tenant` INTEGER DEFAULT NULL,
+ `_createdAt` DATETIME(3) DEFAULT NULL,
+ `_updatedAt` DATETIME(3) DEFAULT NULL,
`_permissions` MEDIUMTEXT DEFAULT NULL".(!empty($attributes) ? ',' : '')."
" . \substr(\implode(' ', $attributeStrings), 0, -2) . "
)
@@ -142,15 +148,17 @@ public function createCollection(string $name, array $attributes = [], array $in
$sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql);
- $this->getPDO()
- ->prepare($sql)
- ->execute();
+ $this->getPDO()->prepare($sql)->execute();
$this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], []);
- $this->createIndex($id, '_created_at', Database::INDEX_KEY, ['_createdAt'], [], []);
- $this->createIndex($id, '_updated_at', Database::INDEX_KEY, ['_updatedAt'], [], []);
+ $this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], []);
+ $this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], []);
+
+ if ($this->shareTables) {
+ $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], []);
+ }
- foreach ($indexes as $key => $index) {
+ foreach ($indexes as $index) {
$indexId = $this->filter($index->getId());
$indexType = $index->getAttribute('type');
$indexAttributes = $index->getAttribute('attributes', []);
@@ -161,8 +169,9 @@ public function createCollection(string $name, array $attributes = [], array $in
}
$sql = "
- CREATE TABLE IF NOT EXISTS `{$namespace}_{$id}_perms` (
+ CREATE TABLE IF NOT EXISTS `{$this->getSQLTable($id)}_perms` (
`_id` INTEGER PRIMARY KEY AUTOINCREMENT,
+ `_tenant` INTEGER DEFAULT NULL,
`_type` VARCHAR(12) NOT NULL,
`_permission` VARCHAR(255) NOT NULL,
`_document` VARCHAR(255) NOT NULL
@@ -176,7 +185,7 @@ public function createCollection(string $name, array $attributes = [], array $in
->execute();
$this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []);
- $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission'], [], []);
+ $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []);
$this->getPDO()->commit();
@@ -199,11 +208,15 @@ public function getSizeOfCollection(string $collection): int
$permissions = $namespace . '_' . $collection . '_perms';
$collectionSize = $this->getPDO()->prepare("
- SELECT SUM(\"pgsize\") FROM \"dbstat\" WHERE name=:name;
+ SELECT SUM(\"pgsize\")
+ FROM \"dbstat\"
+ WHERE name = :name;
");
$permissionsSize = $this->getPDO()->prepare("
- SELECT SUM(\"pgsize\") FROM \"dbstat\" WHERE name=:name;
+ SELECT SUM(\"pgsize\")
+ FROM \"dbstat\"
+ WHERE name = :name;
");
$collectionSize->bindParam(':name', $name);
@@ -237,14 +250,14 @@ public function deleteCollection(string $id): bool
$this->getPDO()->rollBack();
}
- $sql = "DROP TABLE `{$this->getNamespace()}_{$id}`";
+ $sql = "DROP TABLE IF EXISTS `{$this->getSQLTable($id)}`";
$sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql);
$this->getPDO()
->prepare($sql)
->execute();
- $sql = "DROP TABLE `{$this->getNamespace()}_{$id}_perms`";
+ $sql = "DROP TABLE IF EXISTS `{$this->getSQLTable($id)}_perms`";
$sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql);
$this->getPDO()
@@ -379,6 +392,21 @@ public function createIndex(string $collection, string $id, string $type, array
$name = $this->filter($collection);
$id = $this->filter($id);
+
+ // Workaround for no support for CREATE INDEX IF NOT EXISTS
+ $stmt = $this->getPDO()->prepare("
+ SELECT name
+ FROM sqlite_master
+ WHERE type='index'
+ AND name=:_index;
+ ");
+ $stmt->bindValue(':_index', "{$this->getNamespace()}_{$this->tenant}_{$name}_{$id}");
+ $stmt->execute();
+ $index = $stmt->fetch();
+ if (!empty($index)) {
+ return true;
+ }
+
$sql = $this->getSQLIndex($name, $id, $type, $attributes);
$sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql);
@@ -402,7 +430,7 @@ public function deleteIndex(string $collection, string $id): bool
$name = $this->filter($collection);
$id = $this->filter($id);
- $sql = "DROP INDEX `{$this->getNamespace()}_{$name}_{$id}`";
+ $sql = "DROP INDEX `{$this->getNamespace()}_{$this->tenant}_{$name}_{$id}`";
$sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql);
return $this->getPDO()
@@ -423,6 +451,7 @@ public function deleteIndex(string $collection, string $id): bool
public function createDocument(string $collection, Document $document): Document
{
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = json_encode($document->getPermissions());
@@ -465,7 +494,7 @@ public function createDocument(string $collection, Document $document): Document
$stmt->bindValue(':_uid', $document->getId(), PDO::PARAM_STR);
- // Bind manual internal id if set
+ // Bind internal id if set
if (!empty($document->getInternalId())) {
$stmt->bindValue(':_id', $document->getInternalId(), PDO::PARAM_STR);
}
@@ -487,20 +516,19 @@ public function createDocument(string $collection, Document $document): Document
foreach (Database::PERMISSIONS as $type) {
foreach ($document->getPermissionsByType($type) as $permission) {
$permission = \str_replace('"', '', $permission);
- $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}')";
+ $permissions[] = "('{$type}', '{$permission}', '{$document->getId()}', :_tenant)";
}
}
if (!empty($permissions)) {
$queryPermissions = "
- INSERT INTO `{$this->getNamespace()}_{$name}_perms` (_type, _permission, _document)
- VALUES " . \implode(
- ', ',
- $permissions
- );
+ INSERT INTO `{$this->getNamespace()}_{$name}_perms` (_type, _permission, _document, _tenant)
+ VALUES " . \implode(', ', $permissions);
+
$queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions);
$stmtPermissions = $this->getPDO()->prepare($queryPermissions);
+ $stmtPermissions->bindValue(':_tenant', $this->tenant);
}
try {
@@ -509,6 +537,7 @@ public function createDocument(string $collection, Document $document): Document
$statment = $this->getPDO()->prepare("SELECT last_insert_rowid() AS id");
$statment->execute();
$last = $statment->fetch();
+
$document['$internalId'] = $last['id'];
if (isset($stmtPermissions)) {
@@ -542,6 +571,7 @@ public function createDocument(string $collection, Document $document): Document
public function updateDocument(string $collection, Document $document): Document
{
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
$attributes['_permissions'] = json_encode($document->getPermissions());
@@ -556,6 +586,10 @@ public function updateDocument(string $collection, Document $document): Document
WHERE _document = :_uid
";
+ if ($this->shareTables) {
+ $sql .= " AND _tenant = :_tenant";
+ }
+
$sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql);
/**
@@ -563,6 +597,11 @@ public function updateDocument(string $collection, Document $document): Document
*/
$permissionsStmt = $this->getPDO()->prepare($sql);
$permissionsStmt->bindValue(':_uid', $document->getId());
+
+ if ($this->shareTables) {
+ $permissionsStmt->bindValue(':_tenant', $this->tenant);
+ }
+
$permissionsStmt->execute();
$permissions = $permissionsStmt->fetchAll();
$permissionsStmt->closeCursor();
@@ -624,18 +663,26 @@ public function updateDocument(string $collection, Document $document): Document
}
if (!empty($removeQuery)) {
$removeQuery .= ')';
- $removeQuery = "
+ $sql = "
DELETE
FROM `{$this->getNamespace()}_{$name}_perms`
- WHERE
- _document = :_uid
- {$removeQuery}
+ WHERE _document = :_uid
";
+
+ if ($this->shareTables) {
+ $sql .= " AND _tenant = :_tenant";
+ }
+
+ $removeQuery = $sql . $removeQuery;
$removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery);
$stmtRemovePermissions = $this->getPDO()->prepare($removeQuery);
$stmtRemovePermissions->bindValue(':_uid', $document->getId());
+ if ($this->shareTables) {
+ $stmtRemovePermissions->bindValue(':_tenant', $this->tenant);
+ }
+
foreach ($removals as $type => $permissions) {
foreach ($permissions as $i => $permission) {
$stmtRemovePermissions->bindValue(":_remove_{$type}_{$i}", $permission);
@@ -650,19 +697,21 @@ public function updateDocument(string $collection, Document $document): Document
$values = [];
foreach ($additions as $type => $permissions) {
foreach ($permissions as $i => $_) {
- $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i} )";
+ $values[] = "(:_uid, '{$type}', :_add_{$type}_{$i}, :_tenant)";
}
}
$sql = "
- INSERT INTO `{$this->getNamespace()}_{$name}_perms`
- (_document, _type, _permission) VALUES " . \implode(', ', $values);
+ INSERT INTO `{$this->getNamespace()}_{$name}_perms` (_document, _type, _permission, _tenant)
+ VALUES " . \implode(', ', $values);
$sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql);
$stmtAddPermissions = $this->getPDO()->prepare($sql);
$stmtAddPermissions->bindValue(":_uid", $document->getId());
+ $stmtAddPermissions->bindValue(":_tenant", $this->tenant);
+
foreach ($additions as $type => $permissions) {
foreach ($permissions as $i => $permission) {
$stmtAddPermissions->bindValue(":_add_{$type}_{$i}", $permission);
@@ -683,15 +732,24 @@ public function updateDocument(string $collection, Document $document): Document
$sql = "
UPDATE `{$this->getNamespace()}_{$name}`
- SET {$columns} _uid = :_uid WHERE _uid = :_uid
+ SET {$columns} _uid = :_uid
+ WHERE _uid = :_uid
";
+ if ($this->shareTables) {
+ $sql .= " AND _tenant = :_tenant";
+ }
+
$sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql);
$stmt = $this->getPDO()->prepare($sql);
$stmt->bindValue(':_uid', $document->getId());
+ if ($this->shareTables) {
+ $stmt->bindValue(':_tenant', $this->tenant);
+ }
+
$attributeIndex = 0;
foreach ($attributes as $attribute => $value) {
if (is_array($value)) { // arrays & objects should be saved as strings
@@ -717,7 +775,8 @@ public function updateDocument(string $collection, Document $document): Document
$this->getPDO()->rollBack();
throw match ($e->getCode()) {
- '1062', '23000' => new Duplicate('Duplicated document: ' . $e->getMessage()),
+ '1062',
+ '23000' => new Duplicate('Duplicated document: ' . $e->getMessage()),
default => $e,
};
}
@@ -765,6 +824,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($batch as $index => $document) {
$attributes = $document->getAttributes();
+ $attributes['_tenant'] = $this->tenant;
$attributes['_uid'] = $document->getId();
$attributes['_createdAt'] = $document->getCreatedAt();
$attributes['_updatedAt'] = $document->getUpdatedAt();
@@ -790,14 +850,31 @@ public function updateDocuments(string $collection, array $documents, int $batch
$batchKeys[] = '(' . implode(', ', $bindKeys) . ')';
// Permissions logic
- $permissionsStmt = $this->getPDO()->prepare("
+ $sql = "
SELECT _type, _permission
FROM {$this->getSQLTable($name . '_perms')}
WHERE _document = :_uid
- ");
+ ";
+
+ if ($this->shareTables) {
+ $sql .= " AND _tenant = :_tenant";
+ }
+
+ $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql);
+
+ /**
+ * Get current permissions from the database
+ */
+ $permissionsStmt = $this->getPDO()->prepare($sql);
+
$permissionsStmt->bindValue(':_uid', $document->getId());
+
+ if ($this->shareTables) {
+ $permissionsStmt->bindValue(':_tenant', $this->tenant);
+ }
+
$permissionsStmt->execute();
- $permissions = $permissionsStmt->fetchAll(PDO::FETCH_ASSOC);
+ $permissions = $permissionsStmt->fetchAll();
$initial = [];
foreach (Database::PERMISSIONS as $type) {
@@ -825,8 +902,15 @@ public function updateDocuments(string $collection, array $documents, int $batch
$removeBindKeys[] = ':uid_' . $index;
$removeBindValues[$bindKey] = $document->getId();
+
+ $tenantQuery = '';
+ if ($this->shareTables) {
+ $tenantQuery = ' AND _tenant = :_tenant';
+ }
+
$removeQuery .= "(
_document = :uid_{$index}
+ {$tenantQuery}
AND _type = '{$type}'
AND _permission IN (" . implode(', ', \array_map(function (string $i) use ($permissionsToRemove, $index, $type, &$removeBindKeys, &$removeBindValues) {
$bindKey = 'remove_' . $type . '_' . $index . '_' . $i;
@@ -867,7 +951,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
$bindKey = 'add_' . $type . '_' . $index . '_' . $i;
$addBindValues[$bindKey] = $permission;
- $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey})";
+ $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey}, :_tenant)";
if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) {
$addQuery .= ', ';
@@ -889,11 +973,19 @@ public function updateDocuments(string $collection, array $documents, int $batch
$updateClause .= "{$column} = excluded.{$column}";
}
- $stmt = $this->getPDO()->prepare("
+ $sql = "
INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ")
VALUES " . \implode(', ', $batchKeys) . "
- ON CONFLICT(_uid) DO UPDATE SET $updateClause
- ");
+
+ ";
+
+ if ($this->shareTables) {
+ $sql .= "ON CONFLICT (_tenant, _uid) DO UPDATE SET $updateClause";
+ } else {
+ $sql .= "ON CONFLICT (_uid) DO UPDATE SET $updateClause";
+ }
+
+ $stmt = $this->getPDO()->prepare($sql);
foreach ($bindValues as $key => $value) {
$stmt->bindValue($key, $value, $this->getPDOType($value));
@@ -911,26 +1003,29 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($removeBindValues as $key => $value) {
$stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value));
}
+ if ($this->shareTables) {
+ $stmtRemovePermissions->bindValue(':_tenant', $this->tenant);
+ }
$stmtRemovePermissions->execute();
}
if (!empty($addQuery)) {
$stmtAddPermissions = $this->getPDO()->prepare("
- INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission`)
+ INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission, _tenant)
VALUES {$addQuery}
");
foreach ($addBindValues as $key => $value) {
$stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value));
}
-
+ $stmtAddPermissions->bindValue(':_tenant', $this->tenant);
$stmtAddPermissions->execute();
}
}
if (!$this->getPDO()->commit()) {
- throw new Exception('Failed to commit transaction');
+ throw new DatabaseException('Failed to commit transaction');
}
return $documents;
@@ -1033,7 +1128,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr
case Database::INDEX_UNIQUE:
$type = 'UNIQUE INDEX';
- $postfix = ' COLLATE NOCASE';
+ $postfix = 'COLLATE NOCASE';
break;
@@ -1049,13 +1144,20 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr
}, $attributes);
foreach ($attributes as $key => $attribute) {
- $order = '';
$attribute = $this->filter($attribute);
- $attributes[$key] = "`$attribute`$postfix $order";
+ $attributes[$key] = "`{$attribute}` {$postfix}";
}
- return "CREATE {$type} `{$this->getNamespace()}_{$collection}_{$id}` ON `{$this->getNamespace()}_{$collection}` ( " . implode(', ', $attributes) . ")";
+ $key = "`{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}`";
+ $attributes = implode(', ', $attributes);
+
+ if ($this->shareTables) {
+ $key = "`{$this->getNamespace()}_{$collection}_{$id}`";
+ $attributes = "_tenant {$postfix}, {$attributes}";
+ }
+
+ return "CREATE {$type} {$key} ON `{$this->getNamespace()}_{$collection}` ({$attributes})";
}
/**
@@ -1077,6 +1179,17 @@ protected function getSQLPermissionsCondition(string $collection, array $roles):
)";
}
+ /**
+ * Get SQL table
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function getSQLTable(string $name): string
+ {
+ return "{$this->getNamespace()}_{$name}";
+ }
+
/**
* Get list of keywords that cannot be used
* Refference: https://www.sqlite.org/lang_keywords.html
diff --git a/src/Database/Database.php b/src/Database/Database.php
index d6b741d55..ca7920ac1 100644
--- a/src/Database/Database.php
+++ b/src/Database/Database.php
@@ -3,7 +3,6 @@
namespace Utopia\Database;
use Exception;
-use InvalidArgumentException;
use Utopia\Cache\Cache;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization as AuthorizationException;
@@ -142,20 +141,11 @@ class Database
protected array $map = [];
/**
- * @var array
- */
- protected array $primitives = [
- self::VAR_STRING => true,
- self::VAR_INTEGER => true,
- self::VAR_FLOAT => true,
- self::VAR_BOOLEAN => true,
- ];
-
- /**
- * List of Internal Ids
+ * List of Internal attributes
+ *
* @var array>
*/
- protected static array $attributes = [
+ public const INTERNAL_ATTRIBUTES = [
[
'$id' => '$id',
'type' => self::VAR_STRING,
@@ -165,6 +155,15 @@ class Database
'array' => false,
'filters' => [],
],
+ [
+ '$id' => '$internalId',
+ 'type' => self::VAR_STRING,
+ 'size' => Database::LENGTH_KEY,
+ 'required' => true,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
[
'$id' => '$collection',
'type' => self::VAR_STRING,
@@ -174,6 +173,16 @@ class Database
'array' => false,
'filters' => [],
],
+ [
+ '$id' => '$tenant',
+ 'type' => self::VAR_STRING,
+ 'size' => 36,
+ 'required' => false,
+ 'default' => null,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
[
'$id' => '$createdAt',
'type' => Database::VAR_DATETIME,
@@ -195,21 +204,26 @@ class Database
'default' => null,
'array' => false,
'filters' => ['datetime']
- ]
+ ],
+ [
+ '$id' => '$permissions',
+ 'type' => Database::VAR_STRING,
+ 'size' => 1000000,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => [],
+ 'array' => false,
+ 'filters' => ['json']
+ ],
];
- /**
- * List of Internal Attributes
- *
- * @var array
- */
- public const INTERNAL_ATTRIBUTES = [
- '$id',
- '$internalId',
- '$createdAt',
- '$updatedAt',
- '$permissions',
- '$collection',
+ public const INTERNAL_INDEXES = [
+ '_id',
+ '_uid',
+ '_createdAt',
+ '_updatedAt',
+ '_permissions_id',
+ '_permissions',
];
/**
@@ -218,7 +232,7 @@ class Database
*
* @var array
*/
- protected array $collection = [
+ public const COLLECTION = [
'$id' => self::METADATA,
'$collection' => self::METADATA,
'name' => 'collections',
@@ -299,23 +313,23 @@ class Database
protected bool $validate = true;
- private int $relationshipFetchDepth = 1;
+ protected int $relationshipFetchDepth = 1;
/**
* Stack of collection IDs when creating or updating related documents
* @var array
*/
- private array $relationshipWriteStack = [];
+ protected array $relationshipWriteStack = [];
/**
* @var array
*/
- private array $relationshipFetchStack = [];
+ protected array $relationshipFetchStack = [];
/**
* @var array
*/
- private array $relationshipDeleteStack = [];
+ protected array $relationshipDeleteStack = [];
/**
* @param Adapter $adapter
@@ -536,7 +550,7 @@ public function withRequestTimestamp(?\DateTime $requestTimestamp, callable $cal
*
* @return $this
*
- * @throws Exception
+ * @throws DatabaseException
*/
public function setNamespace(string $namespace): self
{
@@ -551,8 +565,6 @@ public function setNamespace(string $namespace): self
* Get namespace of current set scope
*
* @return string
- *
- * @throws DatabaseException
*/
public function getNamespace(): string
{
@@ -563,14 +575,15 @@ public function getNamespace(): string
* Set database to use for current scope
*
* @param string $name
- * @param bool $reset
*
- * @return bool
- * @throws Exception
+ * @return self
+ * @throws DatabaseException
*/
- public function setDefaultDatabase(string $name, bool $reset = false): bool
+ public function setDatabase(string $name): self
{
- return $this->adapter->setDefaultDatabase($name, $reset);
+ $this->adapter->setDatabase($name);
+
+ return $this;
}
/**
@@ -578,13 +591,12 @@ public function setDefaultDatabase(string $name, bool $reset = false): bool
*
* Get Database from current scope
*
- * @throws Exception
- *
* @return string
+ * @throws DatabaseException
*/
- public function getDefaultDatabase(): string
+ public function getDatabase(): string
{
- return $this->adapter->getDefaultDatabase();
+ return $this->adapter->getDatabase();
}
/**
@@ -671,6 +683,36 @@ public function disableValidation(): self
return $this;
}
+ /**
+ * Set Share Tables
+ *
+ * Set whether to share tables between tenants
+ *
+ * @param bool $share
+ * @return self
+ */
+ public function setShareTables(bool $share): self
+ {
+ $this->adapter->setShareTables($share);
+
+ return $this;
+ }
+
+ /**
+ * Set Tenant
+ *
+ * Set tenant to use if tables are shared
+ *
+ * @param ?int $tenant
+ * @return self
+ */
+ public function setTenant(?int $tenant): self
+ {
+ $this->adapter->setTenant($tenant);
+
+ return $this;
+ }
+
/**
* Ping Database
*
@@ -682,38 +724,32 @@ public function ping(): bool
}
/**
- * Create the Default Database
+ * Create the database
*
- * @throws Exception
+ * @throws DatabaseException
*
* @return bool
*/
- public function create(): bool
+ public function create(?string $database = null): bool
{
- $name = $this->adapter->getDefaultDatabase();
- $this->adapter->create($name);
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
+ $database = $database ?? $this->adapter->getDatabase();
+ $this->adapter->create($database);
/**
* Create array of attribute documents
* @var array $attributes
*/
$attributes = array_map(function ($attribute) {
- return new Document([
- '$id' => ID::custom($attribute[0]),
- 'type' => $attribute[1],
- 'size' => $attribute[2],
- 'required' => $attribute[3],
- ]);
- }, [ // Array of [$id, $type, $size, $required]
- ['name', self::VAR_STRING, 512, true],
- ['attributes', self::VAR_STRING, 1000000, false],
- ['indexes', self::VAR_STRING, 1000000, false],
- ['documentSecurity', self::VAR_BOOLEAN, 0, false],
- ]);
+ return new Document($attribute);
+ }, self::COLLECTION['attributes']);
$this->silent(fn () => $this->createCollection(self::METADATA, $attributes));
- $this->trigger(self::EVENT_DATABASE_CREATE, $name);
+ $this->trigger(self::EVENT_DATABASE_CREATE, $database);
return true;
}
@@ -722,13 +758,19 @@ public function create(): bool
* Check if database exists
* Optionally check if collection exists in database
*
- * @param string $database database name
+ * @param string|null $database (optional) database name
* @param string|null $collection (optional) collection name
*
* @return bool
*/
- public function exists(string $database, string $collection = null): bool
+ public function exists(?string $database = null, ?string $collection = null): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
+ $database = $database ?? $this->adapter->getDatabase();
+
return $this->adapter->exists($database, $collection);
}
@@ -739,6 +781,10 @@ public function exists(string $database, string $collection = null): bool
*/
public function list(): array
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$databases = $this->adapter->list();
$this->trigger(self::EVENT_DATABASE_LIST, $databases);
@@ -749,15 +795,23 @@ public function list(): array
/**
* Delete Database
*
- * @param string $name
- *
+ * @param string|null $database
* @return bool
*/
- public function delete(string $name): bool
+ public function delete(?string $database = null): bool
{
- $deleted = $this->adapter->delete($name);
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
+ $database = $database ?? $this->adapter->getDatabase();
+
+ $deleted = $this->adapter->delete($database);
- $this->trigger(self::EVENT_DATABASE_DELETE, ['name' => $name, 'deleted' => $deleted]);
+ $this->trigger(self::EVENT_DATABASE_DELETE, [
+ 'name' => $database,
+ 'deleted' => $deleted
+ ]);
return $deleted;
}
@@ -773,11 +827,14 @@ public function delete(string $name): bool
* @return Document
* @throws DatabaseException
* @throws DuplicateException
- * @throws InvalidArgumentException
* @throws LimitException
*/
public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$permissions ??= [
Permission::create(Role::any()),
];
@@ -785,7 +842,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
if ($this->validate) {
$validator = new Permissions();
if (!$validator->isValid($permissions)) {
- throw new InvalidArgumentException($validator->getDescription());
+ throw new DatabaseException($validator->getDescription());
}
}
@@ -819,7 +876,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
$this->adapter->createCollection($id, $attributes, $indexes);
if ($id === self::METADATA) {
- return new Document($this->collection);
+ return new Document(self::COLLECTION);
}
// Check index limits, if given
@@ -827,7 +884,7 @@ public function createCollection(string $id, array $attributes = [], array $inde
throw new LimitException('Index limit of ' . $this->adapter->getLimitForIndexes() . ' exceeded. Cannot create collection.');
}
- // check attribute limits, if given
+ // Check attribute limits, if given
if ($attributes) {
if (
$this->adapter->getLimitForAttributes() > 0 &&
@@ -859,22 +916,33 @@ public function createCollection(string $id, array $attributes = [], array $inde
* @param bool $documentSecurity
*
* @return Document
- * @throws InvalidArgumentException
* @throws ConflictException
* @throws DatabaseException
- * @throws InvalidArgumentException
*/
public function updateCollection(string $id, array $permissions, bool $documentSecurity): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if ($this->validate) {
$validator = new Permissions();
if (!$validator->isValid($permissions)) {
- throw new InvalidArgumentException($validator->getDescription());
+ throw new DatabaseException($validator->getDescription());
}
}
$collection = $this->silent(fn () => $this->getCollection($id));
+ if ($collection->isEmpty()) {
+ throw new DatabaseException('Collection not found');
+ }
+
+ if ($this->adapter->getShareTables()
+ && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) {
+ throw new DatabaseException('Collection not found');
+ }
+
$collection
->setAttribute('$permissions', $permissions)
->setAttribute('documentSecurity', $documentSecurity);
@@ -896,8 +964,18 @@ public function updateCollection(string $id, array $permissions, bool $documentS
*/
public function getCollection(string $id): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getDocument(self::METADATA, $id));
+ if ($id !== self::METADATA
+ && $this->adapter->getShareTables()
+ && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) {
+ return new Document();
+ }
+
$this->trigger(self::EVENT_COLLECTION_READ, $collection);
return $collection;
@@ -914,11 +992,21 @@ public function getCollection(string $id): Document
*/
public function listCollections(int $limit = 25, int $offset = 0): array
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$result = $this->silent(fn () => $this->find(self::METADATA, [
Query::limit($limit),
Query::offset($offset)
]));
+ if ($this->adapter->getShareTables()) {
+ $result = \array_filter($result, function ($collection) {
+ return $collection->getAttribute('$tenant') === $this->adapter->getTenant();
+ });
+ }
+
$this->trigger(self::EVENT_COLLECTION_LIST, $result);
return $result;
@@ -933,7 +1021,21 @@ public function listCollections(int $limit = 25, int $offset = 0): array
*/
public function getSizeOfCollection(string $collection): int
{
- return $this->adapter->getSizeOfCollection($collection);
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
+ $collection = $this->silent(fn () => $this->getCollection($collection));
+
+ if ($collection->isEmpty()) {
+ throw new DatabaseException('Collection not found');
+ }
+
+ if ($this->adapter->getShareTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) {
+ throw new DatabaseException('Collection not found');
+ }
+
+ return $this->adapter->getSizeOfCollection($collection->getId());
}
/**
@@ -945,8 +1047,20 @@ public function getSizeOfCollection(string $collection): int
*/
public function deleteCollection(string $id): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getDocument(self::METADATA, $id));
+ if ($collection->isEmpty()) {
+ throw new DatabaseException('Collection not found');
+ }
+
+ if ($this->adapter->getShareTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) {
+ throw new DatabaseException('Collection not found');
+ }
+
$relationships = \array_filter(
$collection->getAttribute('attributes'),
fn ($attribute) =>
@@ -991,12 +1105,20 @@ public function deleteCollection(string $id): bool
*/
public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, mixed $default = null, bool $signed = true, bool $array = false, string $format = null, array $formatOptions = [], array $filters = []): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
if ($collection->isEmpty()) {
throw new DatabaseException('Collection not found');
}
+ if ($this->adapter->getShareTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) {
+ throw new DatabaseException('Collection not found');
+ }
+
// attribute IDs are case insensitive
$attributes = $collection->getAttribute('attributes', []);
/** @var array $attributes */
@@ -1164,11 +1286,16 @@ protected function validateDefaultTypes(string $type, mixed $default): void
* @throws ConflictException
* @throws DatabaseException
*/
- private function updateIndexMeta(string $collection, string $id, callable $updateCallback): Document
+ protected function updateIndexMeta(string $collection, string $id, callable $updateCallback): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
+
if ($collection->getId() === self::METADATA) {
- throw new DatabaseException('Cannot update metadata attributes');
+ throw new DatabaseException('Cannot update metadata indexes');
}
$indexes = $collection->getAttribute('indexes', []);
@@ -1202,9 +1329,14 @@ private function updateIndexMeta(string $collection, string $id, callable $updat
* @throws ConflictException
* @throws DatabaseException
*/
- private function updateAttributeMeta(string $collection, string $id, callable $updateCallback): Document
+ protected function updateAttributeMeta(string $collection, string $id, callable $updateCallback): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
+
if ($collection->getId() === self::METADATA) {
throw new DatabaseException('Cannot update metadata attributes');
}
@@ -1439,10 +1571,10 @@ public function updateAttribute(string $collection, string $id, string $type = n
throw new DatabaseException('Failed to update attribute');
}
- $this->deleteCachedCollection($collection);
+ $this->purgeCachedCollection($collection);
}
- $this->deleteCachedDocument(self::METADATA, $collection);
+ $this->purgeCachedDocument(self::METADATA, $collection);
});
}
@@ -1459,6 +1591,10 @@ public function updateAttribute(string $collection, string $id, string $type = n
*/
public function checkAttribute(Document $collection, Document $attribute): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = clone $collection;
$collection->setAttribute('attributes', $attribute, Document::SET_TYPE_APPEND);
@@ -1492,6 +1628,10 @@ public function checkAttribute(Document $collection, Document $attribute): bool
*/
public function deleteAttribute(string $collection, string $id): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
$attributes = $collection->getAttribute('attributes', []);
$indexes = $collection->getAttribute('indexes', []);
@@ -1559,6 +1699,10 @@ public function deleteAttribute(string $collection, string $id): bool
*/
public function renameAttribute(string $collection, string $old, string $new): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
$attributes = $collection->getAttribute('attributes', []);
$indexes = $collection->getAttribute('indexes', []);
@@ -1633,6 +1777,10 @@ public function createRelationship(
?string $twoWayKey = null,
string $onDelete = Database::RELATION_MUTATE_RESTRICT
): bool {
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
if ($collection->isEmpty()) {
@@ -1820,6 +1968,10 @@ public function updateRelationship(
?bool $twoWay = null,
?string $onDelete = null
): bool {
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if (
\is_null($newKey)
&& \is_null($newTwoWayKey)
@@ -1898,7 +2050,7 @@ public function updateRelationship(
$junctionAttribute->setAttribute('key', $newTwoWayKey);
});
- $this->deleteCachedCollection($junction);
+ $this->purgeCachedCollection($junction);
}
if ($altering) {
@@ -1918,8 +2070,8 @@ public function updateRelationship(
}
}
- $this->deleteCachedCollection($collection->getId());
- $this->deleteCachedCollection($relatedCollection->getId());
+ $this->purgeCachedCollection($collection->getId());
+ $this->purgeCachedCollection($relatedCollection->getId());
$renameIndex = function (string $collection, string $key, string $newKey) {
$this->updateIndexMeta(
@@ -1985,6 +2137,10 @@ public function updateRelationship(
*/
public function deleteRelationship(string $collection, string $id): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
$attributes = $collection->getAttribute('attributes', []);
$relationship = null;
@@ -2077,8 +2233,8 @@ public function deleteRelationship(string $collection, string $id): bool
throw new DatabaseException('Failed to delete relationship');
}
- $this->deleteCachedCollection($collection->getId());
- $this->deleteCachedCollection($relatedCollection->getId());
+ $this->purgeCachedCollection($collection->getId());
+ $this->purgeCachedCollection($relatedCollection->getId());
$this->trigger(self::EVENT_ATTRIBUTE_DELETE, $relationship);
@@ -2101,6 +2257,10 @@ public function deleteRelationship(string $collection, string $id): bool
*/
public function renameIndex(string $collection, string $old, string $new): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
$indexes = $collection->getAttribute('indexes', []);
@@ -2160,6 +2320,10 @@ public function renameIndex(string $collection, string $old, string $new): bool
*/
public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if (empty($attributes)) {
throw new DatabaseException('Missing attributes');
}
@@ -2249,6 +2413,10 @@ public function createIndex(string $collection, string $id, string $type, array
*/
public function deleteIndex(string $collection, string $id): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
$indexes = $collection->getAttribute('indexes', []);
@@ -2283,11 +2451,16 @@ public function deleteIndex(string $collection, string $id): bool
*
* @return Document
* @throws DatabaseException
+ * @throws Exception
*/
public function getDocument(string $collection, string $id, array $queries = []): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if ($collection === self::METADATA && $id === self::METADATA) {
- return new Document($this->collection);
+ return new Document(self::COLLECTION);
}
if (empty($collection)) {
@@ -2360,7 +2533,7 @@ public function getDocument(string $collection, string $id, array $queries = [])
$validator = new Authorization(self::PERMISSION_READ);
$documentSecurity = $collection->getAttribute('documentSecurity', false);
- $cacheKey = 'cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $id;
+ $cacheKey = 'cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection->getId() . ':' . $id;
if (!empty($selections)) {
$cacheKey .= ':' . \md5(\implode($selections));
@@ -2430,13 +2603,13 @@ public function getDocument(string $collection, string $id, array $queries = [])
* @phpstan-ignore-next-line
*/
foreach ($this->map as $key => $value) {
- list($k, $v) = explode('=>', $key);
+ [$k, $v] = \explode('=>', $key);
$ck = 'cache-' . $this->getNamespace() . ':map:' . $k;
$cache = $this->cache->load($ck, self::TTL);
if (empty($cache)) {
$cache = [];
}
- if (!in_array($v, $cache)) {
+ if (!\in_array($v, $cache)) {
$cache[] = $v;
$this->cache->save($ck, $cache);
}
@@ -2454,8 +2627,8 @@ public function getDocument(string $collection, string $id, array $queries = [])
if ($query->getMethod() === Query::TYPE_SELECT) {
$values = $query->getValues();
foreach (Database::INTERNAL_ATTRIBUTES as $internalAttribute) {
- if (!in_array($internalAttribute, $values)) {
- $document->removeAttribute($internalAttribute);
+ if (!in_array($internalAttribute['$id'], $values)) {
+ $document->removeAttribute($internalAttribute['$id']);
}
}
}
@@ -2713,6 +2886,10 @@ private function populateDocumentRelationships(Document $collection, Document $d
*/
public function createDocument(string $collection, Document $document): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
if ($collection->getId() !== self::METADATA) {
@@ -2735,7 +2912,7 @@ public function createDocument(string $collection, Document $document): Document
if ($this->validate) {
$validator = new Permissions();
if (!$validator->isValid($document->getPermissions())) {
- throw new InvalidArgumentException($validator->getDescription());
+ throw new DatabaseException($validator->getDescription());
}
}
@@ -2776,6 +2953,10 @@ public function createDocument(string $collection, Document $document): Document
*/
public function createDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE): array
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if (empty($documents)) {
return [];
}
@@ -2785,6 +2966,12 @@ public function createDocuments(string $collection, array $documents, int $batch
$time = DateTime::now();
foreach ($documents as $key => $document) {
+ if ($this->adapter->getShareTables()) {
+ if (empty($document->getAttribute('$tenant'))) {
+ throw new DatabaseException('Missing tenant. Tenant must be included when isolation mode is set to "table".');
+ }
+ }
+
$document
->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId())
->setAttribute('$collection', $collection->getId())
@@ -3071,7 +3258,7 @@ private function relateDocumentsById(
}
break;
case Database::RELATION_MANY_TO_MANY:
- $this->deleteCachedDocument($relatedCollection->getId(), $relationId);
+ $this->purgeCachedDocument($relatedCollection->getId(), $relationId);
$junction = $this->getJunctionCollection($collection, $relatedCollection, $side);
@@ -3103,6 +3290,10 @@ private function relateDocumentsById(
*/
public function updateDocument(string $collection, string $id, Document $document): Document
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if (!$document->getId() || !$id) {
throw new DatabaseException('Must define $id attribute');
}
@@ -3110,11 +3301,13 @@ public function updateDocument(string $collection, string $id, Document $documen
$time = DateTime::now();
$old = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this
$document = \array_merge($old->getArrayCopy(), $document->getArrayCopy());
- $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collectionID
+ $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID
+ $document['$tenant'] = $old->getAttribute('$tenant'); // Make sure user doesn't switch tenant
$document['$createdAt'] = $old->getCreatedAt(); // Make sure user doesn't switch createdAt
$document = new Document($document);
$collection = $this->silent(fn () => $this->getCollection($collection));
+
$relationships = \array_filter($collection->getAttribute('attributes', []), function ($attribute) {
return $attribute['type'] === Database::VAR_RELATIONSHIP;
});
@@ -3246,7 +3439,7 @@ public function updateDocument(string $collection, string $id, Document $documen
$this->purgeRelatedDocuments($collection, $id);
- $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $id . ':*');
+ $this->purgeCachedDocument($collection->getId(), $id);
$this->trigger(self::EVENT_DOCUMENT_UPDATE, $document);
@@ -3268,6 +3461,10 @@ public function updateDocument(string $collection, string $id, Document $documen
*/
public function updateDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE): array
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if (empty($documents)) {
return [];
}
@@ -3276,12 +3473,13 @@ public function updateDocuments(string $collection, array $documents, int $batch
$collection = $this->silent(fn () => $this->getCollection($collection));
foreach ($documents as $document) {
- if (!$document->getId()) {
- throw new Exception('Must define $id attribute for each document');
+ if ($this->adapter->getShareTables() && empty($document->getAttribute('$tenant'))) {
+ throw new DatabaseException('Missing tenant. Tenant must be included when isolation mode is set to "table".');
}
- $document->setAttribute('$updatedAt', $time);
- $document = $this->encode($collection, $document);
+ if (!$document->getId()) {
+ throw new DatabaseException('Must define $id attribute for each document');
+ }
$old = Authorization::skip(fn () => $this->silent(
fn () => $this->getDocument(
@@ -3290,6 +3488,9 @@ public function updateDocuments(string $collection, array $documents, int $batch
)
));
+ $document->setAttribute('$updatedAt', $time);
+ $document = $this->encode($collection, $document);
+
$validator = new Authorization(self::PERMISSION_UPDATE);
if ($collection->getId() !== self::METADATA
&& !$validator->isValid($old->getUpdate())) {
@@ -3307,7 +3508,7 @@ public function updateDocuments(string $collection, array $documents, int $batch
foreach ($documents as $key => $document) {
$documents[$key] = $this->decode($collection, $document);
- $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $document->getId() . ':*');
+ $this->purgeCachedDocument($collection->getId(), $document->getId());
}
$this->trigger(self::EVENT_DOCUMENTS_UPDATE, $documents);
@@ -3565,7 +3766,7 @@ private function updateDocumentRelationships(Document $collection, Document $old
// For many-one we need to update the related key to null if no relation exists
$document->setAttribute($key, null);
}
- $this->deleteCachedDocument($relatedCollection->getId(), $value);
+ $this->purgeCachedDocument($relatedCollection->getId(), $value);
} elseif ($value instanceof Document) {
$related = $this->getDocument($relatedCollection->getId(), $value->getId());
@@ -3583,12 +3784,15 @@ private function updateDocumentRelationships(Document $collection, Document $old
$related->getId(),
$value
);
- $this->deleteCachedDocument($relatedCollection->getId(), $related->getId());
+ $this->purgeCachedDocument($relatedCollection->getId(), $related->getId());
}
$document->setAttribute($key, $value->getId());
} elseif (\is_null($value)) {
break;
+ } elseif (empty($value)) {
+ throw new DatabaseException('Invalid value for relationship');
+
} else {
throw new DatabaseException('Invalid value for relationship');
}
@@ -3710,6 +3914,10 @@ private function getJunctionCollection(Document $collection, Document $relatedCo
*/
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if ($value <= 0) { // Can be a float
throw new DatabaseException('Value must be numeric and greater than 0');
}
@@ -3754,7 +3962,8 @@ public function increaseDocumentAttribute(string $collection, string $id, string
$max = $max ? $max - $value : null;
$result = $this->adapter->increaseDocumentAttribute($collection->getId(), $id, $attribute, $value, null, $max);
- $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $id . ':*');
+
+ $this->purgeCachedDocument($collection->getId(), $id);
$this->trigger(self::EVENT_DOCUMENT_INCREASE, $document);
@@ -3777,6 +3986,10 @@ public function increaseDocumentAttribute(string $collection, string $id, string
*/
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool
{
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
if ($value <= 0) { // Can be a float
throw new DatabaseException('Value must be numeric and greater than 0');
}
@@ -3821,7 +4034,7 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
$min = $min ? $min + $value : null;
$result = $this->adapter->increaseDocumentAttribute($collection->getId(), $id, $attribute, $value * -1, $min);
- $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $id . ':*');
+ $this->purgeCachedDocument($collection->getId(), $id);
$this->trigger(self::EVENT_DOCUMENT_DECREASE, $document);
return $result;
@@ -3843,7 +4056,12 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
*/
public function deleteDocument(string $collection, string $id): bool
{
- $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
+ $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id)));
+
$collection = $this->silent(fn () => $this->getCollection($collection));
$validator = new Authorization(self::PERMISSION_DELETE);
@@ -3876,7 +4094,7 @@ public function deleteDocument(string $collection, string $id): bool
$deleted = $this->adapter->deleteDocument($collection->getId(), $id);
$this->purgeRelatedDocuments($collection, $id);
- $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $id . ':*');
+ $this->purgeCachedDocument($collection->getId(), $id);
$this->trigger(self::EVENT_DOCUMENT_DELETE, $document);
@@ -4260,9 +4478,9 @@ private function deleteCascade(Document $collection, Document $relatedCollection
* @return bool
* @throws DatabaseException
*/
- public function deleteCachedCollection(string $collection): bool
+ public function purgeCachedCollection(string $collection): bool
{
- return $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection . ':*');
+ return $this->cache->purge('cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection . ':*');
}
/**
@@ -4274,9 +4492,9 @@ public function deleteCachedCollection(string $collection): bool
* @return bool
* @throws DatabaseException
*/
- public function deleteCachedDocument(string $collection, string $id): bool
+ public function purgeCachedDocument(string $collection, string $id): bool
{
- return $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection . ':' . $id . ':*');
+ return $this->cache->purge('cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection . ':' . $id . ':*');
}
/**
@@ -4292,11 +4510,14 @@ public function deleteCachedDocument(string $collection, string $id): bool
*/
public function find(string $collection, array $queries = []): array
{
- $originalName = $collection;
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
+ }
+
$collection = $this->silent(fn () => $this->getCollection($collection));
if ($collection->isEmpty()) {
- throw new DatabaseException('Collection "'. $originalName .'" not found');
+ throw new DatabaseException('Collection not found');
}
$attributes = $collection->getAttribute('attributes', []);
@@ -4414,14 +4635,16 @@ public function find(string $collection, array $queries = []): array
}
}
+ unset($query);
+
// Remove internal attributes which are not queried
foreach ($queries as $query) {
if ($query->getMethod() === Query::TYPE_SELECT) {
$values = $query->getValues();
foreach ($results as $result) {
foreach (Database::INTERNAL_ATTRIBUTES as $internalAttribute) {
- if (!\in_array($internalAttribute, $values)) {
- $result->removeAttribute($internalAttribute);
+ if (!\in_array($internalAttribute['$id'], $values)) {
+ $result->removeAttribute($internalAttribute['$id']);
}
}
}
@@ -4466,12 +4689,11 @@ public function findOne(string $collection, array $queries = []): false|Document
*/
public function count(string $collection, array $queries = [], ?int $max = null): int
{
- $collection = $this->silent(fn () => $this->getCollection($collection));
-
- if ($collection->isEmpty()) {
- throw new DatabaseException("Collection not found");
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
}
+ $collection = $this->silent(fn () => $this->getCollection($collection));
$attributes = $collection->getAttribute('attributes', []);
$indexes = $collection->getAttribute('indexes', []);
@@ -4513,12 +4735,11 @@ public function count(string $collection, array $queries = [], ?int $max = null)
*/
public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int
{
- $collection = $this->silent(fn () => $this->getCollection($collection));
-
- if ($collection->isEmpty()) {
- throw new DatabaseException("Collection not found");
+ if ($this->adapter->getShareTables() && empty($this->adapter->getTenant())) {
+ throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
}
+ $collection = $this->silent(fn () => $this->getCollection($collection));
$attributes = $collection->getAttribute('attributes', []);
$indexes = $collection->getAttribute('indexes', []);
@@ -4555,19 +4776,6 @@ public static function addFilter(string $name, callable $encode, callable $decod
];
}
- /**
- * @return array
- * @throws DatabaseException
- */
- public static function getInternalAttributes(): array
- {
- $attributes = [];
- foreach (self::$attributes as $internal) {
- $attributes[] = new Document($internal);
- }
- return $attributes;
- }
-
/**
* Encode Document
*
@@ -4580,7 +4788,14 @@ public static function getInternalAttributes(): array
public function encode(Document $collection, Document $document): Document
{
$attributes = $collection->getAttribute('attributes', []);
- $attributes = array_merge($attributes, $this->getInternalAttributes());
+
+ $internalAttributes = \array_filter(Database::INTERNAL_ATTRIBUTES, function ($attribute) {
+ // We don't want to encode permissions into a JSON string
+ return $attribute['$id'] !== '$permissions';
+ });
+
+ $attributes = \array_merge($attributes, $internalAttributes);
+
foreach ($attributes as $attribute) {
$key = $attribute['$id'] ?? '';
$array = $attribute['array'] ?? false;
@@ -4657,7 +4872,7 @@ public function decode(Document $collection, Document $document, array $selectio
}
}
- $attributes = array_merge($attributes, $this->getInternalAttributes());
+ $attributes = array_merge($attributes, Database::INTERNAL_ATTRIBUTES);
foreach ($attributes as $attribute) {
$key = $attribute['$id'] ?? '';
@@ -4846,10 +5061,11 @@ private function validateSelections(Document $collection, array $queries): array
}
}
- $keys = [];
-
// Allow querying internal attributes
- $keys = array_merge($keys, self::INTERNAL_ATTRIBUTES);
+ $keys = \array_map(
+ fn ($attribute) => $attribute['$id'],
+ self::INTERNAL_ATTRIBUTES
+ );
foreach ($collection->getAttribute('attributes', []) as $attribute) {
if ($attribute['type'] !== self::VAR_RELATIONSHIP) {
@@ -4992,7 +5208,7 @@ private function purgeRelatedDocuments(Document $collection, string $id): void
if (!empty($cache)) {
foreach ($cache as $v) {
list($collectionId, $documentId) = explode(':', $v);
- $this->deleteCachedDocument($collectionId, $documentId);
+ $this->purgeCachedDocument($collectionId, $documentId);
}
$this->cache->purge($key);
}
diff --git a/src/Database/Document.php b/src/Database/Document.php
index 6311841b4..7afddc25f 100644
--- a/src/Database/Document.php
+++ b/src/Database/Document.php
@@ -4,8 +4,6 @@
use ArrayObject;
use Utopia\Database\Exception as DatabaseException;
-use Utopia\Database\Helpers\Permission;
-use Utopia\Database\Helpers\Role;
/**
* @extends ArrayObject
@@ -168,15 +166,13 @@ public function getAttributes(): array
{
$attributes = [];
+ $internalKeys = \array_map(
+ fn ($attr) => $attr['$id'],
+ Database::INTERNAL_ATTRIBUTES
+ );
+
foreach ($this as $attribute => $value) {
- if (\array_key_exists($attribute, [
- '$id' => true,
- '$internalId' => true,
- '$collection' => true,
- '$permissions' => [Permission::read(Role::any())],
- '$createdAt' => true,
- '$updatedAt' => true,
- ])) {
+ if (\in_array($attribute, $internalKeys)) {
continue;
}
diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php
index a0b6b80f5..6cdf9d89c 100644
--- a/src/Database/Validator/Index.php
+++ b/src/Database/Validator/Index.php
@@ -20,6 +20,7 @@ class Index extends Validator
/**
* @param array $attributes
* @param int $maxLength
+ * @throws DatabaseException
*/
public function __construct(array $attributes, int $maxLength)
{
@@ -29,9 +30,9 @@ public function __construct(array $attributes, int $maxLength)
$key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id')));
$this->attributes[$key] = $attribute;
}
- foreach (Database::getInternalAttributes() as $attribute) {
- $key = \strtolower($attribute->getAttribute('$id'));
- $this->attributes[$key] = $attribute;
+ foreach (Database::INTERNAL_ATTRIBUTES as $attribute) {
+ $key = \strtolower($attribute['$id']);
+ $this->attributes[$key] = new Document($attribute);
}
}
diff --git a/src/Database/Validator/Queries/Document.php b/src/Database/Validator/Queries/Document.php
index fbb164d9d..41c9f3f9b 100644
--- a/src/Database/Validator/Queries/Document.php
+++ b/src/Database/Validator/Queries/Document.php
@@ -21,14 +21,12 @@ public function __construct(array $attributes)
'type' => Database::VAR_STRING,
'array' => false,
]);
-
$attributes[] = new \Utopia\Database\Document([
'$id' => '$createdAt',
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
-
$attributes[] = new \Utopia\Database\Document([
'$id' => '$updatedAt',
'key' => '$updatedAt',
diff --git a/src/Database/Validator/Query/Select.php b/src/Database/Validator/Query/Select.php
index 833284c5e..e00c3916e 100644
--- a/src/Database/Validator/Query/Select.php
+++ b/src/Database/Validator/Query/Select.php
@@ -2,6 +2,7 @@
namespace Utopia\Database\Validator\Query;
+use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
@@ -56,6 +57,11 @@ public function isValid($value): bool
return false;
}
+ $internalKeys = \array_map(
+ fn ($attr) => $attr['$id'],
+ Database::INTERNAL_ATTRIBUTES
+ );
+
foreach ($value->getValues() as $attribute) {
if (\str_contains($attribute, '.')) {
//special symbols with `dots`
@@ -69,7 +75,7 @@ public function isValid($value): bool
}
// Skip internal attributes
- if (\in_array($attribute, self::INTERNAL_ATTRIBUTES)) {
+ if (\in_array($attribute, $internalKeys)) {
continue;
}
diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php
index 14223b1a5..3d7d2ade7 100644
--- a/src/Database/Validator/Structure.php
+++ b/src/Database/Validator/Structure.php
@@ -52,6 +52,16 @@ class Structure extends Validator
'array' => false,
'filters' => [],
],
+ [
+ '$id' => '$tenant',
+ 'type' => Database::VAR_STRING,
+ 'size' => 36,
+ 'required' => false,
+ 'default' => null,
+ 'signed' => true,
+ 'array' => false,
+ 'filters' => [],
+ ],
[
'$id' => '$permissions',
'type' => Database::VAR_STRING,
@@ -208,7 +218,7 @@ public function isValid($document): bool
}
if (empty($this->collection->getId()) || Database::METADATA !== $this->collection->getCollection()) {
- $this->message = 'Collection "'.$this->collection->getCollection().'" not found';
+ $this->message = 'Collection not found';
return false;
}
diff --git a/tests/Database/Base.php b/tests/e2e/Adapter/Base.php
similarity index 98%
rename from tests/Database/Base.php
rename to tests/e2e/Adapter/Base.php
index f38f70b25..95cdd9cae 100644
--- a/tests/Database/Base.php
+++ b/tests/e2e/Adapter/Base.php
@@ -1,34 +1,37 @@
getDatabase()->getAdapter()->getSupportForSchemas();
if (!$schemaSupport) {
- $this->assertEquals(true, static::getDatabase()->setDefaultDatabase($this->testDatabase));
+ $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase));
$this->assertEquals(true, static::getDatabase()->create());
return;
}
@@ -71,7 +74,7 @@ public function testCreateExistsDelete(): void
$this->assertEquals(true, static::getDatabase()->exists($this->testDatabase));
$this->assertEquals(true, static::getDatabase()->delete($this->testDatabase));
$this->assertEquals(false, static::getDatabase()->exists($this->testDatabase));
- $this->assertEquals(true, static::getDatabase()->setDefaultDatabase($this->testDatabase));
+ $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase));
$this->assertEquals(true, static::getDatabase()->create());
}
@@ -233,7 +236,7 @@ public function testQueryTimeout(): void
for ($i = 0 ; $i <= 20 ; $i++) {
static::getDatabase()->createDocument('global-timeouts', new Document([
- 'longtext' => file_get_contents(__DIR__ . '/../resources/longtext.txt'),
+ 'longtext' => file_get_contents(__DIR__ . '/../../resources/longtext.txt'),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
@@ -242,7 +245,7 @@ public function testQueryTimeout(): void
]));
}
- $this->expectException(Timeout::class);
+ $this->expectException(TimeoutException::class);
static::getDatabase()->setTimeout(1);
@@ -250,7 +253,7 @@ public function testQueryTimeout(): void
static::getDatabase()->find('global-timeouts', [
Query::notEqual('longtext', 'appwrite'),
]);
- } catch(Timeout $ex) {
+ } catch(TimeoutException $ex) {
static::getDatabase()->clearTimeout();
static::getDatabase()->deleteCollection('global-timeouts');
throw $ex;
@@ -523,7 +526,7 @@ public function testCollectionNotFound(): void
static::getDatabase()->find('not_exist', []);
$this->fail('Failed to throw Exception');
} catch (Exception $e) {
- $this->assertEquals('Collection "not_exist" not found', $e->getMessage());
+ $this->assertEquals('Collection not found', $e->getMessage());
}
}
@@ -3943,7 +3946,7 @@ public function testGetAttributeLimit(): void
public function testGetIndexLimit(): void
{
- $this->assertEquals(59, $this->getDatabase()->getLimitForIndexes());
+ $this->assertEquals(58, $this->getDatabase()->getLimitForIndexes());
}
public function testGetId(): void
@@ -11411,7 +11414,7 @@ public function testCollectionUpdate(): Document
*/
public function testCollectionUpdatePermissionsThrowException(Document $collection): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(DatabaseException::class);
static::getDatabase()->updateCollection($collection->getId(), permissions: [
'i dont work'
], documentSecurity: false);
@@ -11441,7 +11444,7 @@ public function testCollectionPermissions(): Document
public function testCollectionPermissionsExceptions(): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(DatabaseException::class);
static::getDatabase()->createCollection('collectionSecurity', permissions: [
'i dont work'
]);
@@ -12596,6 +12599,160 @@ public function testEmptyOperatorValues(): void
}
}
+ /**
+ * @throws AuthorizationException
+ * @throws DatabaseException
+ * @throws DuplicateException
+ * @throws LimitException
+ * @throws QueryException
+ * @throws StructureException
+ * @throws TimeoutException
+ */
+ public function testIsolationModes(): void
+ {
+ /**
+ * Default mode already tested, we'll test 'schema' and 'table' isolation here
+ */
+ $database = static::getDatabase();
+
+ if ($database->exists('schema1')) {
+ $database->setDatabase('schema1')->delete();
+ }
+ if ($database->exists('schema2')) {
+ $database->setDatabase('schema2')->delete();
+ }
+ if ($database->exists('sharedTables')) {
+ $database->setDatabase('sharedTables')->delete();
+ }
+
+ /**
+ * Schema
+ */
+ $database
+ ->setDatabase('schema1')
+ ->setNamespace('')
+ ->create();
+
+ if ($database->getAdapter()->getSupportForSchemas()) {
+ $this->assertEquals(true, $database->exists('schema1'));
+ }
+
+ $database
+ ->setDatabase('schema2')
+ ->setNamespace('')
+ ->create();
+
+ if ($database->getAdapter()->getSupportForSchemas()) {
+ $this->assertEquals(true, $database->exists('schema2'));
+ }
+
+ /**
+ * Table
+ */
+
+ $tenant1 = 1;
+ $tenant2 = 2;
+
+ $database
+ ->setDatabase('sharedTables')
+ ->setNamespace('')
+ ->setShareTables(true)
+ ->setTenant($tenant1)
+ ->create();
+
+ if ($database->getAdapter()->getSupportForSchemas()) {
+ $this->assertEquals(true, $database->exists('sharedTables'));
+ }
+
+ $database->createCollection('people', [
+ new Document([
+ '$id' => 'name',
+ 'type' => Database::VAR_STRING,
+ 'size' => 128,
+ 'required' => true,
+ ]),
+ new Document([
+ '$id' => 'lifeStory',
+ 'type' => Database::VAR_STRING,
+ 'size' => 65536,
+ 'required' => true,
+ ])
+ ], [
+ new Document([
+ '$id' => 'idx_name',
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['name']
+ ])
+ ]);
+
+ if ($database->getAdapter()->getSupportForFulltextIndex()) {
+ $database->createIndex(
+ collection: 'people',
+ id: 'idx_lifeStory',
+ type: Database::INDEX_FULLTEXT,
+ attributes: ['lifeStory']
+ );
+ }
+
+ $docId = ID::unique();
+
+ $database->createDocument('people', new Document([
+ '$id' => $docId,
+ '$permissions' => [
+ Permission::read(Role::any()),
+ ],
+ 'name' => 'Spiderman',
+ 'lifeStory' => 'Spider-Man is a superhero appearing in American comic books published by Marvel Comics.'
+ ]));
+
+ $doc = $database->getDocument('people', $docId);
+ $this->assertEquals('Spiderman', $doc['name']);
+ $this->assertEquals($tenant1, $doc->getAttribute('$tenant'));
+
+ $docs = $database->find('people');
+ $this->assertEquals(1, \count($docs));
+
+ // Swap to tenant 2, no access
+ $database->setTenant($tenant2);
+
+ try {
+ $database->getDocument('people', $docId);
+ $this->fail('Failed to throw exception');
+ } catch (Exception $e) {
+ $this->assertEquals('Collection not found', $e->getMessage());
+ }
+
+ try {
+ $database->find('people');
+ $this->fail('Failed to throw exception');
+ } catch (Exception $e) {
+ $this->assertEquals('Collection not found', $e->getMessage());
+ }
+
+ // Swap back to tenant 1, allowed
+ $database->setTenant($tenant1);
+
+ $doc = $database->getDocument('people', $docId);
+ $this->assertEquals('Spiderman', $doc['name']);
+ $docs = $database->find('people');
+ $this->assertEquals(1, \count($docs));
+
+ // Remove tenant but leave shared tables enabled
+ $database->setTenant(null);
+
+ try {
+ $database->getDocument('people', $docId);
+ $this->fail('Failed to throw exception');
+ } catch (Exception $e) {
+ $this->assertEquals('Missing tenant. Tenant must be set when table sharing is enabled.', $e->getMessage());
+ }
+
+ // Reset state
+ $database->setShareTables(false);
+ $database->setNamespace(static::$namespace);
+ $database->setDatabase($this->testDatabase);
+ }
+
public function testTransformations(): void
{
static::getDatabase()->createCollection('docs', attributes: [
@@ -12658,7 +12815,7 @@ public function testEvents(): void
});
if ($this->getDatabase()->getAdapter()->getSupportForSchemas()) {
- $database->setDefaultDatabase('hellodb');
+ $database->setDatabase('hellodb');
$database->create();
} else {
array_shift($events);
@@ -12666,7 +12823,7 @@ public function testEvents(): void
$database->list();
- $database->setDefaultDatabase($this->testDatabase);
+ $database->setDatabase($this->testDatabase);
$collectionId = ID::unique();
$database->createCollection($collectionId);
diff --git a/tests/Database/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php
similarity index 75%
rename from tests/Database/Adapter/MariaDBTest.php
rename to tests/e2e/Adapter/MariaDBTest.php
index d5014432e..e26ca69c2 100644
--- a/tests/Database/Adapter/MariaDBTest.php
+++ b/tests/e2e/Adapter/MariaDBTest.php
@@ -1,20 +1,19 @@
setDefaultDatabase('utopiaTests');
- $database->setNamespace('myapp_' . uniqid());
+ $database->setDatabase('utopiaTests');
+ $database->setNamespace(static::$namespace = 'myapp_' . uniqid());
if ($database->exists('utopiaTests')) {
$database->delete('utopiaTests');
diff --git a/tests/Database/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php
similarity index 87%
rename from tests/Database/Adapter/MongoDBTest.php
rename to tests/e2e/Adapter/MongoDBTest.php
index 62c48ae37..887c26ec2 100644
--- a/tests/Database/Adapter/MongoDBTest.php
+++ b/tests/e2e/Adapter/MongoDBTest.php
@@ -1,20 +1,19 @@
setDefaultDatabase($schema);
- $database->setNamespace('myapp_' . uniqid());
+ $database->setDatabase($schema);
+ $database->setNamespace(static::$namespace = 'myapp_' . uniqid());
if ($database->exists('utopiaTests')) {
$database->delete('utopiaTests');
@@ -73,7 +72,7 @@ public function testCreateExistsDelete(): void
$this->assertNotNull(static::getDatabase()->create());
$this->assertEquals(true, static::getDatabase()->delete($this->testDatabase));
$this->assertEquals(true, static::getDatabase()->create());
- $this->assertEquals(true, static::getDatabase()->setDefaultDatabase($this->testDatabase));
+ $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase));
}
public function testRenameAttribute(): void
diff --git a/tests/Database/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php
similarity index 77%
rename from tests/Database/Adapter/MySQLTest.php
rename to tests/e2e/Adapter/MySQLTest.php
index 0faefca61..d204e8a40 100644
--- a/tests/Database/Adapter/MySQLTest.php
+++ b/tests/e2e/Adapter/MySQLTest.php
@@ -1,20 +1,19 @@
setDefaultDatabase('utopiaTests');
- $database->setNamespace('myapp_'.uniqid());
+ $database->setDatabase('utopiaTests');
+ $database->setNamespace(static::$namespace = 'myapp_' . uniqid());
if ($database->exists('utopiaTests')) {
$database->delete('utopiaTests');
diff --git a/tests/Database/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php
similarity index 86%
rename from tests/Database/Adapter/PostgresTest.php
rename to tests/e2e/Adapter/PostgresTest.php
index 9965a8684..6a12ecd8c 100644
--- a/tests/Database/Adapter/PostgresTest.php
+++ b/tests/e2e/Adapter/PostgresTest.php
@@ -1,18 +1,18 @@
setDefaultDatabase('utopiaTests');
- $database->setNamespace('myapp_'.uniqid());
+ $database->setDatabase('utopiaTests');
+ $database->setNamespace(static::$namespace = 'myapp_' . uniqid());
if ($database->exists('utopiaTests')) {
$database->delete('utopiaTests');
diff --git a/tests/Database/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php
similarity index 61%
rename from tests/Database/Adapter/SQLiteTest.php
rename to tests/e2e/Adapter/SQLiteTest.php
index 8a8d24199..16c36f6fd 100644
--- a/tests/Database/Adapter/SQLiteTest.php
+++ b/tests/e2e/Adapter/SQLiteTest.php
@@ -1,20 +1,19 @@
connect('redis', 6379);
+ $redis->connect('redis');
$redis->flushAll();
$cache = new Cache(new RedisAdapter($redis));
$database = new Database(new SQLite($pdo), $cache);
- $database->setDefaultDatabase('utopiaTests');
- $database->setNamespace('myapp_'.uniqid());
+ $database->setDatabase('utopiaTests');
+ $database->setNamespace(static::$namespace = 'myapp_' . uniqid());
+
+ if ($database->exists()) {
+ $database->delete();
+ }
+
+ $database->create();
return self::$database = $database;
}
diff --git a/tests/Database/DocumentTest.php b/tests/unit/DocumentTest.php
similarity index 99%
rename from tests/Database/DocumentTest.php
rename to tests/unit/DocumentTest.php
index acb9e7be0..4d2e517fc 100644
--- a/tests/Database/DocumentTest.php
+++ b/tests/unit/DocumentTest.php
@@ -1,10 +1,10 @@
'team@appwrite.io',
])));
- $this->assertEquals('Invalid document structure: Collection "" not found', $validator->getDescription());
+ $this->assertEquals('Invalid document structure: Collection not found', $validator->getDescription());
}
public function testRequiredKeys(): void
diff --git a/tests/Database/Validator/UIDTest.php b/tests/unit/Validator/UIDTest.php
similarity index 55%
rename from tests/Database/Validator/UIDTest.php
rename to tests/unit/Validator/UIDTest.php
index e7c357bb3..c88fd9563 100644
--- a/tests/Database/Validator/UIDTest.php
+++ b/tests/unit/Validator/UIDTest.php
@@ -1,6 +1,6 @@