From a7d7aee72951ff4e0b6b6b136daf4871436f1a48 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 6 Jun 2024 00:06:14 +1200 Subject: [PATCH 001/256] Add mirroring filter --- src/Database/Database.php | 1 + src/Database/Mirroring/Filter.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/Database/Mirroring/Filter.php diff --git a/src/Database/Database.php b/src/Database/Database.php index 0236b9d92..dfafb8874 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -915,6 +915,7 @@ public function list(): array * * @param string|null $database * @return bool + * @throws DatabaseException */ public function delete(?string $database = null): bool { diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php new file mode 100644 index 000000000..848dd7561 --- /dev/null +++ b/src/Database/Mirroring/Filter.php @@ -0,0 +1,29 @@ + Date: Thu, 6 Jun 2024 00:07:00 +1200 Subject: [PATCH 002/256] Add mirror database --- src/Database/Mirror.php | 422 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 src/Database/Mirror.php diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php new file mode 100644 index 000000000..db2fe58c1 --- /dev/null +++ b/src/Database/Mirror.php @@ -0,0 +1,422 @@ + + */ + protected array $writeFilters = []; + + /** + * Collections that should only be present in the source database + */ + protected const SOURCE_ONLY_COLLECTIONS = [ + 'upgrades', + ]; + + /** + * Callbacks to run when an error occurs on the destination database + * + * @var array + */ + protected array $errorCallbacks = []; + + /** + * @param Adapter $adapter + * @param Cache $cache + * @param Database $destination + * @param array $filters + */ + public function __construct( + Adapter $adapter, + Cache $cache, + Database $destination, + array $filters = [], + ) { + parent::__construct($adapter, $cache); + $this->destination = $destination; + $this->writeFilters = $filters; + } + + public function getDestination(): ?Database + { + return $this->destination; + } + + public function getSource(): Database + { + return $this; + } + + /** + * @param callable(string, \Throwable): void $callback + * @return void + */ + public function onError(callable $callback): void + { + $this->errorCallbacks[] = $callback; + } + + /** + * @param string $method + * @param array $args + * @return mixed + */ + protected function delegate(string $method, array $args = []): mixed + { + $result = parent::{$method}(...$args); + + try { + $result = $this->destination->{$method}(...$args); + } catch (\Throwable $err) { + $this->logError($method, $err); + } + + return $result; + } + + public function enableValidation(): self + { + return $this->delegate('enableValidation'); + } + + public function disableValidation(): self + { + return $this->delegate('disableValidation'); + } + + public function delete(?string $database = null): bool + { + return $this->delegate('delete', [$database]); + } + + public function create(?string $database = null): bool + { + return $this->delegate('create', [$database]); + } + + public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document + { + $result = parent::createCollection( + $id, + $attributes, + $indexes, + $permissions, + $documentSecurity + ); + + try { + $this->destination->createCollection( + $id, + $attributes, + $indexes, + $permissions, + $documentSecurity + ); + + $this->createUpgrades(); + + parent::createDocument('upgrades', new Document([ + '$id' => $id, + 'collectionId' => $id, + 'status' => 'upgraded' + ])); + } catch (\Throwable $err) { + $this->logError('createCollection', $err); + } + return $result; + } + + public function updateCollection(string $id, array $permissions, bool $documentSecurity): Document + { + return $this->delegate('updateCollection', [$id, $permissions, $documentSecurity]); + } + + public function deleteCollection(string $id): bool + { + return $this->delegate('deleteCollection', [$id]); + } + + public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, string $format = null, array $formatOptions = [], array $filters = []): bool + { + return $this->delegate('createAttribute', [$collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters]); + } + + public function updateAttribute(string $collection, string $id, string $type = null, int $size = null, bool $required = null, mixed $default = null, bool $signed = null, bool $array = null, string $format = null, ?array $formatOptions = null, ?array $filters = null): Document + { + return $this->delegate('updateAttribute', [$collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters]); + } + + public function deleteAttribute(string $collection, string $id): bool + { + return $this->delegate('deleteAttribute', [$collection, $id]); + } + + public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool + { + return $this->delegate('createIndex', [$collection, $id, $type, $attributes, $lengths, $orders]); + } + + public function deleteIndex(string $collection, string $id): bool + { + return $this->delegate('deleteIndex', [$collection, $id]); + } + + public function createDocument(string $collection, Document $document): Document + { + $document = parent::createDocument($collection, $document); + + if (\in_array($collection, self::SOURCE_ONLY_COLLECTIONS)) { + return $document; + } + + $upgrade = $this->getUpgradeStatus($collection); + if ($upgrade->getAttribute('status', '') !== 'upgraded') { + return $document; + } + + try { + $clone = clone $document; + + foreach ($this->writeFilters as $filter) { + $clone = $filter->onCreateDocument( + source: $this, + destination: $this->destination, + collection: $collection, + document: $clone, + ); + } + + $this->destination->setPreserveDates(true); + $this->destination->createDocument($collection, $clone); + $this->destination->setPreserveDates(false); + } catch (\Throwable $err) { + $this->logError('createDocument', $err); + } + + return $document; + } + + public function updateDocument(string $collection, string $id, Document $document): Document + { + $document = parent::updateDocument($collection, $id, $document); + + $upgrade = $this->getUpgradeStatus($collection); + if ($upgrade->getAttribute('status', '') !== 'upgraded') { + return $document; + } + + try { + $clone = clone $document; + + foreach ($this->writeFilters as $filter) { + $clone = $filter->onUpdateDocument( + source: $this, + destination: $this->destination, + collection: $collection, + document: $clone, + ); + } + + if (!$this->destination->getDocument($collection, $id)->isEmpty()) { + $this->destination->setPreserveDates(true); + $this->destination->updateDocument($collection, $id, $clone); + $this->destination->setPreserveDates(false); + } + } catch (\Throwable $err) { + $this->logError('updateDocument', $err); + } + + return $document; + } + + public function deleteDocument(string $collection, string $id): bool + { + $result = parent::deleteDocument($collection, $id); + + $upgrade = $this->getUpgradeStatus($collection); + if ($upgrade->getAttribute('status', '') !== 'upgraded') { + return $result; + } + + try { + $this->destination->deleteDocument($collection, $id); + + foreach ($this->writeFilters as $filter) { + $filter->onDeleteDocument( + source: $this, + destination: $this->destination, + collection: $collection, + ); + } + } catch (\Throwable $err) { + $this->logError('deleteDocument', $err); + } + + return $result; + } + + public function updateAttributeRequired(string $collection, string $id, bool $required): Document + { + return $this->delegate('updateAttributeRequired', [$collection, $id, $required]); + } + + public function updateAttributeFormat(string $collection, string $id, string $format): Document + { + return $this->delegate('updateAttributeFormat', [$collection, $id, $format]); + } + + public function updateAttributeFormatOptions(string $collection, string $id, array $formatOptions): Document + { + return $this->delegate('updateAttributeFormatOptions', [$collection, $id, $formatOptions]); + } + + public function updateAttributeFilters(string $collection, string $id, array $filters): Document + { + return $this->delegate('updateAttributeFilters', [$collection, $id, $filters]); + } + + public function updateAttributeDefault(string $collection, string $id, mixed $default = null): Document + { + return $this->delegate('updateAttributeDefault', [$collection, $id, $default]); + } + + public function renameAttribute(string $collection, string $old, string $new): bool + { + return $this->delegate('renameAttribute', [$collection, $old, $new]); + } + + public function createRelationship( + string $collection, + string $relatedCollection, + string $type, + bool $twoWay = false, + ?string $id = null, + ?string $twoWayKey = null, + string $onDelete = Database::RELATION_MUTATE_RESTRICT + ): bool { + return $this->delegate('createRelationship', [$collection, $relatedCollection, $type, $twoWay, $id, $twoWayKey, $onDelete]); + } + + public function updateRelationship( + string $collection, + string $id, + ?string $newKey = null, + ?string $newTwoWayKey = null, + ?bool $twoWay = null, + ?string $onDelete = null + ): bool { + return $this->delegate('updateRelationship', [$collection, $id, $newKey, $newTwoWayKey, $twoWay, $onDelete]); + } + + public function deleteRelationship(string $collection, string $id): bool + { + return $this->delegate('deleteRelationship', [$collection, $id]); + } + + + public function renameIndex(string $collection, string $old, string $new): bool + { + return $this->delegate('renameIndex', [$collection, $old, $new]); + } + + public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool + { + return $this->delegate('increaseDocumentAttribute', [$collection, $id, $attribute, $value, $max]); + } + + public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool + { + return $this->delegate('decreaseDocumentAttribute', [$collection, $id, $attribute, $value, $min]); + } + + /** + * @throws Limit + * @throws DuplicateException + * @throws Exception + */ + public function createUpgrades(): void + { + try { + parent::createCollection( + id: 'upgrades', + attributes: [ + new Document([ + '$id' => ID::custom('collectionId'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + 'default' => null, + 'format' => '' + ]), + new Document([ + '$id' => ID::custom('status'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + 'default' => null, + 'format' => '' + ]), + ], + indexes: [ + new Document([ + '$id' => ID::custom('_unique_collection'), + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['collectionId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [], + ]), + new Document([ + '$id' => ID::custom('_status_index'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['status'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ]), + ], + ); + } catch (DuplicateException) { + // Ignore + } + } + + /** + * @throws Exception + */ + protected function getUpgradeStatus(string $collection): Document + { + if ($collection === 'upgrades' || $collection === Database::METADATA) { + return new Document([]); + } + + return Authorization::skip(function () use ($collection) { + return $this->getDocument('upgrades', $collection); + }); + } + + protected function logError(string $action, \Throwable $err): void + { + foreach ($this->errorCallbacks as $callback) { + $callback($action, $err); + } + } +} From d2dc913fe70f14cfc621a7f6c105c2d1b9420681 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 6 Jun 2024 20:57:14 +1200 Subject: [PATCH 003/256] Add source property so filters call Database methods on source db instead of Mirror methods --- src/Database/Mirror.php | 31 ++++++++++++++------------- src/Database/Mirroring/Filter.php | 35 ++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index db2fe58c1..c06019417 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -11,6 +11,7 @@ class Mirror extends Database { + protected Database $source; protected Database $destination; /** @@ -20,13 +21,6 @@ class Mirror extends Database */ protected array $writeFilters = []; - /** - * Collections that should only be present in the source database - */ - protected const SOURCE_ONLY_COLLECTIONS = [ - 'upgrades', - ]; - /** * Callbacks to run when an error occurs on the destination database * @@ -34,6 +28,13 @@ class Mirror extends Database */ protected array $errorCallbacks = []; + /** + * Collections that should only be present in the source database + */ + protected const SOURCE_ONLY_COLLECTIONS = [ + 'upgrades', + ]; + /** * @param Adapter $adapter * @param Cache $cache @@ -47,6 +48,7 @@ public function __construct( array $filters = [], ) { parent::__construct($adapter, $cache); + $this->source = new Database($adapter, $cache); $this->destination = $destination; $this->writeFilters = $filters; } @@ -58,7 +60,7 @@ public function getDestination(): ?Database public function getSource(): Database { - return $this; + return $this->source; } /** @@ -193,9 +195,9 @@ public function createDocument(string $collection, Document $document): Document foreach ($this->writeFilters as $filter) { $clone = $filter->onCreateDocument( - source: $this, + source: $this->source, destination: $this->destination, - collection: $collection, + collectionId: $collection, document: $clone, ); } @@ -224,9 +226,9 @@ public function updateDocument(string $collection, string $id, Document $documen foreach ($this->writeFilters as $filter) { $clone = $filter->onUpdateDocument( - source: $this, + source: $this->source, destination: $this->destination, - collection: $collection, + collectionId: $collection, document: $clone, ); } @@ -257,9 +259,10 @@ public function deleteDocument(string $collection, string $id): bool foreach ($this->writeFilters as $filter) { $filter->onDeleteDocument( - source: $this, + source: $this->source, destination: $this->destination, - collection: $collection, + collectionId: $collection, + documentId: $id, ); } } catch (\Throwable $err) { diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index 848dd7561..f247b9220 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -7,23 +7,52 @@ abstract class Filter { + /** + * Called before document is created in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param Document $document + * @return Document + */ abstract public function onCreateDocument( Database $source, Database $destination, - string $collection, + string $collectionId, Document $document, ): Document; + + /** + * Called before document is updated in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param Document $document + * @return Document + */ abstract public function onUpdateDocument( Database $source, Database $destination, - string $collection, + string $collectionId, Document $document, ): Document; + /** + * Called after document is deleted in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $documentId + * @return void + */ abstract public function onDeleteDocument( Database $source, Database $destination, - string $collection, + string $collectionId, + string $documentId, ): void; } From 2e02f0b8064ce2a36c1982a949696c5d82e7a853 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 6 Jun 2024 21:07:39 +1200 Subject: [PATCH 004/256] Use source instead of parent so changes applied via getSource are used --- src/Database/Database.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index dfafb8874..c331f5af7 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3507,11 +3507,13 @@ public function updateDocument(string $collection, string $id, Document $documen $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['$createdAt'] = $old->getCreatedAt(); // Make sure user doesn't switch createdAt $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID + if($this->adapter->getSharedTables()) { - $document['$tenant'] = $old->getAttribute('$tenant'); // Make sure user doesn't switch tenant + $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)); From ba9fc4e5ab4c5f33aed90ba402956074c2dc936a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 6 Jun 2024 22:05:19 +1200 Subject: [PATCH 005/256] Add back nullable destination --- src/Database/Mirror.php | 77 ++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index c06019417..005b58a5c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -2,7 +2,6 @@ namespace Utopia\Database; -use Utopia\Cache\Cache; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit; use Utopia\Database\Helpers\ID; @@ -12,7 +11,7 @@ class Mirror extends Database { protected Database $source; - protected Database $destination; + protected ?Database $destination; /** * Filters to apply to documents before writing to the destination database @@ -36,31 +35,32 @@ class Mirror extends Database ]; /** - * @param Adapter $adapter - * @param Cache $cache - * @param Database $destination + * @param Database $source + * @param ?Database $destination * @param array $filters */ public function __construct( - Adapter $adapter, - Cache $cache, - Database $destination, + Database $source, + ?Database $destination = null, array $filters = [], ) { - parent::__construct($adapter, $cache); - $this->source = new Database($adapter, $cache); + parent::__construct( + $source->getAdapter(), + $source->getCache() + ); + $this->source = $source; $this->destination = $destination; $this->writeFilters = $filters; } - public function getDestination(): ?Database + public function getSource(): Database { - return $this->destination; + return $this->source; } - public function getSource(): Database + public function getDestination(): ?Database { - return $this->source; + return $this->destination; } /** @@ -79,7 +79,11 @@ public function onError(callable $callback): void */ protected function delegate(string $method, array $args = []): mixed { - $result = parent::{$method}(...$args); + $result = $this->source->{$method}(...$args); + + if ($this->destination === null) { + return $result; + } try { $result = $this->destination->{$method}(...$args); @@ -112,7 +116,7 @@ public function create(?string $database = null): bool public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document { - $result = parent::createCollection( + $result = $this->source->createCollection( $id, $attributes, $indexes, @@ -120,6 +124,10 @@ public function createCollection(string $id, array $attributes = [], array $inde $documentSecurity ); + if ($this->destination === null) { + return $result; + } + try { $this->destination->createCollection( $id, @@ -131,7 +139,7 @@ public function createCollection(string $id, array $attributes = [], array $inde $this->createUpgrades(); - parent::createDocument('upgrades', new Document([ + $this->source->createDocument('upgrades', new Document([ '$id' => $id, 'collectionId' => $id, 'status' => 'upgraded' @@ -179,9 +187,12 @@ public function deleteIndex(string $collection, string $id): bool public function createDocument(string $collection, Document $document): Document { - $document = parent::createDocument($collection, $document); + $document = $this->source->createDocument($collection, $document); - if (\in_array($collection, self::SOURCE_ONLY_COLLECTIONS)) { + if ( + \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) + || $this->destination === null + ) { return $document; } @@ -214,7 +225,14 @@ public function createDocument(string $collection, Document $document): Document public function updateDocument(string $collection, string $id, Document $document): Document { - $document = parent::updateDocument($collection, $id, $document); + $document = $this->source->updateDocument($collection, $id, $document); + + if ( + \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) + || $this->destination === null + ) { + return $document; + } $upgrade = $this->getUpgradeStatus($collection); if ($upgrade->getAttribute('status', '') !== 'upgraded') { @@ -233,11 +251,9 @@ public function updateDocument(string $collection, string $id, Document $documen ); } - if (!$this->destination->getDocument($collection, $id)->isEmpty()) { - $this->destination->setPreserveDates(true); - $this->destination->updateDocument($collection, $id, $clone); - $this->destination->setPreserveDates(false); - } + $this->destination->setPreserveDates(true); + $this->destination->updateDocument($collection, $id, $clone); + $this->destination->setPreserveDates(false); } catch (\Throwable $err) { $this->logError('updateDocument', $err); } @@ -247,7 +263,14 @@ public function updateDocument(string $collection, string $id, Document $documen public function deleteDocument(string $collection, string $id): bool { - $result = parent::deleteDocument($collection, $id); + $result = $this->source->deleteDocument($collection, $id); + + if ( + \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) + || $this->destination === null + ) { + return $result; + } $upgrade = $this->getUpgradeStatus($collection); if ($upgrade->getAttribute('status', '') !== 'upgraded') { @@ -354,7 +377,7 @@ public function decreaseDocumentAttribute(string $collection, string $id, string public function createUpgrades(): void { try { - parent::createCollection( + $this->source->createCollection( id: 'upgrades', attributes: [ new Document([ From c409fc51a19ce5d9cb5af3accad1f4ff150f63df Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 6 Jun 2024 22:09:36 +1200 Subject: [PATCH 006/256] Add base tests --- src/Database/Mirror.php | 10 ++ tests/e2e/Adapter/MirrorTest.php | 156 +++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 tests/e2e/Adapter/MirrorTest.php diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 005b58a5c..a19ef1528 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -94,6 +94,16 @@ protected function delegate(string $method, array $args = []): mixed return $result; } + public function setDatabase(string $name): Database + { + return $this->delegate('setDatabase', [$name]); + } + + public function setNamespace(string $namespace): Database + { + return $this->delegate('setNamespace', [$namespace]); + } + public function enableValidation(): self { return $this->delegate('enableValidation'); diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php new file mode 100644 index 000000000..258241945 --- /dev/null +++ b/tests/e2e/Adapter/MirrorTest.php @@ -0,0 +1,156 @@ +connect('redis'); + $redis->flushAll(); + $cache = new Cache(new RedisAdapter($redis)); + + self::$source = new Database(new MariaDB($pdo), $cache); + + $mirrorHost = 'mariadb-mirror'; + $mirrorPort = '3306'; + $mirrorUser = 'root'; + $mirrorPass = 'password'; + + $mirrorPdo = new PDO("mysql:host={$mirrorHost};port={$mirrorPort};charset=utf8mb4", $mirrorUser, $mirrorPass, MariaDB::getPDOAttributes()); + + self::$destination = new Database(new MariaDB($mirrorPdo), $cache); + + $database = new Mirror(self::$source, self::$destination); + + $database + ->setDatabase('utopiaTests') + ->setNamespace(static::$namespace = 'myapp_' . uniqid()); + + if ($database->exists()) { + $database->delete(); + } + + $database->create(); + + return self::$database = $database; + } + + protected static function getAdapterName(): string + { + return "Mirror"; + } + + /** + * @throws Exception + * @throws \RedisException + */ + public function testGetSource(): void + { + $database = self::getDatabase(); + $source = $database->getSource(); + $this->assertInstanceOf(Database::class, $source); + $this->assertEquals(self::$source, $source); + } + + /** + * @throws Exception + * @throws \RedisException + */ + public function testGetDestination(): void + { + $database = self::getDatabase(); + $destination = $database->getDestination(); + $this->assertInstanceOf(Database::class, $destination); + $this->assertEquals(self::$destination, $destination); + } + + /** + * @throws Limit + * @throws Duplicate + * @throws Exception + * @throws \RedisException + */ + public function testCreateCollection(): void + { + $database = self::getDatabase(); + + $database->createCollection('testCreateCollection'); + + // Assert collection exists in both databases + $this->assertFalse($database->getSource()->getCollection('testCreateCollection')->isEmpty()); + $this->assertFalse($database->getDestination()->getCollection('testCreateCollection')->isEmpty()); + } + + /** + * @throws Limit + * @throws Duplicate + * @throws \RedisException + * @throws Conflict + * @throws Exception + */ + public function testUpdateCollection(): void + { + $database = self::getDatabase(); + + $database->createCollection('testUpdateCollection', permissions: [ + Permission::read(Role::any()), + ]); + + $collection = $database->getCollection('testUpdateCollection'); + + $database->updateCollection( + 'testUpdateCollection', + [ + Permission::read(Role::users()), + ], + $collection->getAttribute('documentSecurity') + ); + + // Asset both databases have updated the collection + $this->assertEquals( + [Permission::read(Role::users())], + $database->getSource()->getCollection('testUpdateCollection')->getPermissions() + ); + + $this->assertEquals( + [Permission::read(Role::users())], + $database->getDestination()->getCollection('testUpdateCollection')->getPermissions() + ); + } +} From 712f3e8f6264c678c9314edeba776b9875b4ec5d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 17:36:55 +1200 Subject: [PATCH 007/256] Add xdebug --- Dockerfile | 47 +++++++++++++++++++++++++++++++++++----------- composer.lock | 2 +- dev/xdebug.ini | 8 ++++++++ docker-compose.yml | 5 +++-- 4 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 dev/xdebug.ini diff --git a/Dockerfile b/Dockerfile index 443bc3d13..7c46fd8f9 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,42 @@ FROM composer:2.0 as composer -ARG TESTING=false -ENV TESTING=$TESTING +ARG DEBUG=false +ENV DEBUG=$DEBUG WORKDIR /usr/local/src/ COPY composer.lock /usr/local/src/ COPY composer.json /usr/local/src/ -RUN composer install --ignore-platform-reqs --optimize-autoloader \ - --no-plugins --no-scripts --prefer-dist +RUN composer install \ + --ignore-platform-reqs \ + --optimize-autoloader \ + --no-plugins \ + --no-scripts \ + --prefer-dist -FROM php:8.3.3-cli-alpine3.19 as compile +FROM php:8.3.7-cli-alpine3.19 as compile + +ENV PHP_REDIS_VERSION="6.0.2" \ + PHP_SWOOLE_VERSION="v5.1.3" \ + PHP_MONGO_VERSION="1.16.1" \ + PHP_XDEBUG_VERSION="3.3.2" -ENV PHP_REDIS_VERSION=6.0.2 \ - PHP_SWOOLE_VERSION=v5.1.2 \ - PHP_MONGO_VERSION=1.16.1 - RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone RUN \ apk update \ - && apk add --no-cache postgresql-libs postgresql-dev make automake autoconf gcc g++ git brotli-dev \ + && apk add --no-cache \ + postgresql-libs \ + postgresql-dev \ + make \ + automake \ + autoconf \ + gcc \ + g++ \ + git \ + brotli-dev \ + linux-headers \ && docker-php-ext-install opcache pgsql pdo_mysql pdo_pgsql \ && apk del postgresql-dev \ && rm -rf /var/cache/apk/* @@ -63,6 +78,15 @@ RUN \ && ./configure --enable-pcov \ && make && make install +## XDebug Extension +FROM compile AS xdebug +RUN \ + git clone --depth 1 --branch $PHP_XDEBUG_VERSION https://github.com/xdebug/xdebug && \ + cd xdebug && \ + phpize && \ + ./configure && \ + make && make install + FROM compile as final LABEL maintainer="team@appwrite.io" @@ -73,6 +97,7 @@ RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini RUN echo extension=swoole.so >> /usr/local/etc/php/conf.d/swoole.ini RUN echo extension=mongodb.so >> /usr/local/etc/php/conf.d/mongodb.ini RUN echo extension=pcov.so >> /usr/local/etc/php/conf.d/pcov.ini +RUN echo extension=xdebug.so >> /usr/local/etc/php/conf.d/xdebug.ini RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" @@ -85,8 +110,8 @@ COPY --from=swoole /usr/local/lib/php/extensions/no-debug-non-zts-20230831/swool COPY --from=redis /usr/local/lib/php/extensions/no-debug-non-zts-20230831/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20230831/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ COPY --from=pcov /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcov.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ +COPY --from=xdebug /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ -# Add Source Code COPY ./bin /usr/src/code/bin COPY ./src /usr/src/code/src diff --git a/composer.lock b/composer.lock index 37d72aa6d..53540fd01 100644 --- a/composer.lock +++ b/composer.lock @@ -2603,5 +2603,5 @@ "php": ">=8.0" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/dev/xdebug.ini b/dev/xdebug.ini new file mode 100644 index 000000000..0f3241d37 --- /dev/null +++ b/dev/xdebug.ini @@ -0,0 +1,8 @@ +zend_extension = xdebug.so + +[xdebug] +xdebug.mode = develop,debug +xdebug.start_with_request = yes +xdebug.client_host=host.docker.internal +xdebug.client_port = 9003 +xdebug.log = /tmp/xdebug.log \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c5a9128f7..4d10e8cb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ services: image: databases-dev build: context: . + args: + - DEBUG=true networks: - database volumes: @@ -11,8 +13,7 @@ services: - ./src:/usr/src/code/src - ./tests:/usr/src/code/tests - ./phpunit.xml:/usr/src/code/phpunit.xml - ports: - - "8708:8708" + - ./dev/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini adminer: image: adminer From f812b84203f2a29fd5891f92649ba68d1922147a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 18:30:45 +1200 Subject: [PATCH 008/256] Fix getting upgrade status --- src/Database/Mirror.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index a19ef1528..448018ee9 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -438,14 +438,18 @@ public function createUpgrades(): void /** * @throws Exception */ - protected function getUpgradeStatus(string $collection): Document + protected function getUpgradeStatus(string $collection): ?Document { if ($collection === 'upgrades' || $collection === Database::METADATA) { - return new Document([]); + return new Document(); } return Authorization::skip(function () use ($collection) { - return $this->getDocument('upgrades', $collection); + try { + return $this->getDocument('upgrades', $collection); + } catch (\Throwable) { + return null; + } }); } From 757fa9113e69ad7f3661113bb85503ed9d0db15b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 18:31:43 +1200 Subject: [PATCH 009/256] Add missing create/update batch methods --- src/Database/Mirror.php | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 448018ee9..f68291c99 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -233,6 +233,53 @@ public function createDocument(string $collection, Document $document): Document return $document; } + public function createDocuments( + string $collection, + array $documents, + int $batchSize = self::INSERT_BATCH_SIZE + ): array { + $documents = $this->source->createDocuments($collection, $documents, $batchSize); + + if ( + \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) + || $this->destination === null + ) { + return $documents; + } + + $upgrade = $this->getUpgradeStatus($collection); + if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { + return $documents; + } + + try { + $clones = []; + + foreach ($documents as $document) { + $clone = clone $document; + + foreach ($this->writeFilters as $filter) { + $clone = $filter->onCreateDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + document: $clone, + ); + } + + $clones[] = $clone; + } + + $this->destination->setPreserveDates(true); + $this->destination->createDocuments($collection, $clones, $batchSize); + $this->destination->setPreserveDates(false); + } catch (\Throwable $err) { + $this->logError('createDocuments', $err); + } + + return $documents; + } + public function updateDocument(string $collection, string $id, Document $document): Document { $document = $this->source->updateDocument($collection, $id, $document); @@ -271,6 +318,53 @@ public function updateDocument(string $collection, string $id, Document $documen return $document; } + public function updateDocuments( + string $collection, + array $documents, + int $batchSize = self::INSERT_BATCH_SIZE + ): array { + $documents = $this->source->updateDocuments($collection, $documents, $batchSize); + + if ( + \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) + || $this->destination === null + ) { + return $documents; + } + + $upgrade = $this->getUpgradeStatus($collection); + if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { + return $documents; + } + + try { + $clones = []; + + foreach ($documents as $document) { + $clone = clone $document; + + foreach ($this->writeFilters as $filter) { + $clone = $filter->onUpdateDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + document: $clone, + ); + } + + $clones[] = $clone; + } + + $this->destination->setPreserveDates(true); + $this->destination->updateDocuments($collection, $clones, $batchSize); + $this->destination->setPreserveDates(false); + } catch (\Throwable $err) { + $this->logError('updateDocuments', $err); + } + + return $documents; + } + public function deleteDocument(string $collection, string $id): bool { $result = $this->source->deleteDocument($collection, $id); From 5af4e8e862095d6979d3136061d834599346565d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 18:32:19 +1200 Subject: [PATCH 010/256] Fix upgrade checks --- src/Database/Mirror.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index f68291c99..09d070d8d 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -207,7 +207,7 @@ public function createDocument(string $collection, Document $document): Document } $upgrade = $this->getUpgradeStatus($collection); - if ($upgrade->getAttribute('status', '') !== 'upgraded') { + if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $document; } @@ -292,7 +292,7 @@ public function updateDocument(string $collection, string $id, Document $documen } $upgrade = $this->getUpgradeStatus($collection); - if ($upgrade->getAttribute('status', '') !== 'upgraded') { + if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $document; } @@ -377,7 +377,7 @@ public function deleteDocument(string $collection, string $id): bool } $upgrade = $this->getUpgradeStatus($collection); - if ($upgrade->getAttribute('status', '') !== 'upgraded') { + if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $result; } From 995be2e26437705259ce2caf431ddc1b213992ad Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 18:36:09 +1200 Subject: [PATCH 011/256] Delegate more setters and delete --- src/Database/Mirror.php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 09d070d8d..1564fd277 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -104,7 +104,22 @@ public function setNamespace(string $namespace): Database return $this->delegate('setNamespace', [$namespace]); } - public function enableValidation(): self + public function setSharedTables(bool $sharedTables): static + { + return $this->delegate('setSharedTables', [$sharedTables]); + } + + public function setTenant(?int $tenant): static + { + return $this->delegate('setTenant', [$tenant]); + } + + public function setPreserveDates(bool $preserve): static + { + return $this->delegate('setPreserveDates', [$preserve]); + } + + public function enableValidation(): static { return $this->delegate('enableValidation'); } @@ -114,9 +129,9 @@ public function disableValidation(): self return $this->delegate('disableValidation'); } - public function delete(?string $database = null): bool + public function exists(?string $database = null, ?string $collection = null): bool { - return $this->delegate('delete', [$database]); + return $this->delegate('exists', [$database, $collection]); } public function create(?string $database = null): bool @@ -124,6 +139,11 @@ public function create(?string $database = null): bool return $this->delegate('create', [$database]); } + public function delete(?string $database = null): bool + { + return $this->delegate('delete', [$database]); + } + public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document { $result = $this->source->createCollection( From b5fae84d9f669468b26507318653a4680325d747 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 18:51:23 +1200 Subject: [PATCH 012/256] Static returns --- src/Database/Adapter.php | 14 +++++----- src/Database/Database.php | 42 ++++++++++++++--------------- src/Database/Document.php | 13 ++++----- src/Database/Helpers/Permission.php | 2 +- src/Database/Helpers/Role.php | 14 +++++----- src/Database/Mirror.php | 8 +++--- tests/e2e/Adapter/Base.php | 6 ++--- 7 files changed, 49 insertions(+), 50 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index ee138b7e0..586040828 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -38,7 +38,7 @@ abstract class Adapter * * @return $this */ - public function setDebug(string $key, mixed $value): self + public function setDebug(string $key, mixed $value): static { $this->debug[$key] = $value; @@ -54,9 +54,9 @@ public function getDebug(): array } /** - * @return self + * @return static */ - public function resetDebug(): self + public function resetDebug(): static { $this->debug = []; @@ -192,7 +192,7 @@ public function getTenant(): ?int * @param mixed $value * @return $this */ - public function setMetadata(string $key, mixed $value): self + public function setMetadata(string $key, mixed $value): static { $this->metadata[$key] = $value; @@ -223,7 +223,7 @@ public function getMetadata(): array * * @return $this */ - public function resetMetadata(): self + public function resetMetadata(): static { $this->metadata = []; @@ -236,9 +236,9 @@ public function resetMetadata(): self * @param string $event * @param string $name * @param ?callable $callback - * @return self + * @return static */ - public function before(string $event, string $name = '', ?callable $callback = null): self + public function before(string $event, string $name = '', ?callable $callback = null): static { if (!isset($this->transformations[$event])) { $this->transformations[$event] = []; diff --git a/src/Database/Database.php b/src/Database/Database.php index faa316cc8..868131d07 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -430,9 +430,9 @@ function (?string $value) { * @param string $event * @param string $name * @param callable $callback - * @return self + * @return static */ - public function on(string $event, string $name, callable $callback): self + public function on(string $event, string $name, callable $callback): static { if (!isset($this->listeners[$event])) { $this->listeners[$event] = []; @@ -450,7 +450,7 @@ public function on(string $event, string $name, callable $callback): self * @param callable $callback * @return $this */ - public function before(string $event, string $name, callable $callback): self + public function before(string $event, string $name, callable $callback): static { $this->adapter->before($event, $name, $callback); @@ -563,7 +563,7 @@ public function withRequestTimestamp(?\DateTime $requestTimestamp, callable $cal * * @throws DatabaseException */ - public function setNamespace(string $namespace): self + public function setNamespace(string $namespace): static { $this->adapter->setNamespace($namespace); @@ -587,10 +587,10 @@ public function getNamespace(): string * * @param string $name * - * @return self + * @return static * @throws DatabaseException */ - public function setDatabase(string $name): self + public function setDatabase(string $name): static { $this->adapter->setDatabase($name); @@ -617,7 +617,7 @@ public function getDatabase(): string * * @return $this */ - public function setCache(Cache $cache): self + public function setCache(Cache $cache): static { $this->cache = $cache; return $this; @@ -639,7 +639,7 @@ public function getCache(): Cache * @param string $name * @return $this */ - public function setCacheName(string $name): self + public function setCacheName(string $name): static { $this->cacheName = $name; @@ -661,9 +661,9 @@ public function getCacheName(): string * * @param string $key * @param mixed $value - * @return self + * @return static */ - public function setMetadata(string $key, mixed $value): self + public function setMetadata(string $key, mixed $value): static { $this->adapter->setMetadata($key, $value); @@ -695,10 +695,10 @@ public function resetMetadata(): void * * @param int $milliseconds * @param string $event - * @return self + * @return static * @throws Exception */ - public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): self + public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): static { $this->adapter->setTimeout($milliseconds, $event); @@ -721,7 +721,7 @@ public function clearTimeout(string $event = Database::EVENT_ALL): void * * @return $this */ - public function enableFilters(): self + public function enableFilters(): static { $this->filter = true; @@ -733,7 +733,7 @@ public function enableFilters(): self * * @return $this */ - public function disableFilters(): self + public function disableFilters(): static { $this->filter = false; @@ -755,7 +755,7 @@ public function getInstanceFilters(): array * * @return $this */ - public function enableValidation(): self + public function enableValidation(): static { $this->validate = true; @@ -767,7 +767,7 @@ public function enableValidation(): self * * @return $this */ - public function disableValidation(): self + public function disableValidation(): static { $this->validate = false; @@ -780,9 +780,9 @@ public function disableValidation(): self * Set whether to share tables between tenants * * @param bool $sharedTables - * @return self + * @return static */ - public function setSharedTables(bool $sharedTables): self + public function setSharedTables(bool $sharedTables): static { $this->adapter->setSharedTables($sharedTables); @@ -795,16 +795,16 @@ public function setSharedTables(bool $sharedTables): self * Set tenant to use if tables are shared * * @param ?int $tenant - * @return self + * @return static */ - public function setTenant(?int $tenant): self + public function setTenant(?int $tenant): static { $this->adapter->setTenant($tenant); return $this; } - public function setPreserveDates(bool $preserve): self + public function setPreserveDates(bool $preserve): static { $this->preserveDates = $preserve; diff --git a/src/Database/Document.php b/src/Database/Document.php index 7afddc25f..f605f615f 100644 --- a/src/Database/Document.php +++ b/src/Database/Document.php @@ -210,9 +210,9 @@ public function getAttribute(string $name, mixed $default = null): mixed * @param mixed $value * @param string $type * - * @return self + * @return static */ - public function setAttribute(string $key, mixed $value, string $type = self::SET_TYPE_ASSIGN): self + public function setAttribute(string $key, mixed $value, string $type = self::SET_TYPE_ASSIGN): static { switch ($type) { case self::SET_TYPE_ASSIGN: @@ -235,9 +235,9 @@ public function setAttribute(string $key, mixed $value, string $type = self::SET * Set Attributes. * * @param array $attributes - * @return self + * @return static */ - public function setAttributes(array $attributes): self + public function setAttributes(array $attributes): static { foreach ($attributes as $key => $value) { $this->setAttribute($key, $value); @@ -253,14 +253,15 @@ public function setAttributes(array $attributes): self * * @param string $key * - * @return self + * @return static */ - public function removeAttribute(string $key): self + public function removeAttribute(string $key): static { if (\array_key_exists($key, (array)$this)) { unset($this[$key]); } + /* @phpstan-ignore-next-line */ return $this; } diff --git a/src/Database/Helpers/Permission.php b/src/Database/Helpers/Permission.php index df0f1a5c0..18c4fe5a9 100644 --- a/src/Database/Helpers/Permission.php +++ b/src/Database/Helpers/Permission.php @@ -77,7 +77,7 @@ public function getDimension(): string * Parse a permission string into a Permission object * * @param string $permission - * @return Permission + * @return self * @throws Exception */ public static function parse(string $permission): self diff --git a/src/Database/Helpers/Role.php b/src/Database/Helpers/Role.php index aaf755038..1682cb547 100644 --- a/src/Database/Helpers/Role.php +++ b/src/Database/Helpers/Role.php @@ -56,7 +56,7 @@ public function getDimension(): string * Parse a role string into a Role object * * @param string $role - * @return Role + * @return self * @throws \Exception */ public static function parse(string $role): self @@ -110,7 +110,7 @@ public static function parse(string $role): self * * @param string $identifier * @param string $status - * @return Role + * @return self */ public static function user(string $identifier, string $status = ''): Role { @@ -121,7 +121,7 @@ public static function user(string $identifier, string $status = ''): Role * Create a users role * * @param string $status - * @return Role + * @return self */ public static function users(string $status = ''): self { @@ -133,7 +133,7 @@ public static function users(string $status = ''): self * * @param string $identifier * @param string $dimension - * @return Role + * @return self */ public static function team(string $identifier, string $dimension = ''): self { @@ -144,7 +144,7 @@ public static function team(string $identifier, string $dimension = ''): self * Create a label role from the given ID * * @param string $identifier - * @return Role + * @return self */ public static function label(string $identifier): self { @@ -154,7 +154,7 @@ public static function label(string $identifier): self /** * Create an any satisfy role * - * @return Role + * @return self */ public static function any(): Role { @@ -164,7 +164,7 @@ public static function any(): Role /** * Create a guests role * - * @return Role + * @return self */ public static function guests(): self { diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 1564fd277..659b8fc54 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -94,12 +94,12 @@ protected function delegate(string $method, array $args = []): mixed return $result; } - public function setDatabase(string $name): Database + public function setDatabase(string $name): static { return $this->delegate('setDatabase', [$name]); } - public function setNamespace(string $namespace): Database + public function setNamespace(string $namespace): static { return $this->delegate('setNamespace', [$namespace]); } @@ -124,7 +124,7 @@ public function enableValidation(): static return $this->delegate('enableValidation'); } - public function disableValidation(): self + public function disableValidation(): static { return $this->delegate('disableValidation'); } @@ -562,7 +562,7 @@ protected function getUpgradeStatus(string $collection): ?Document try { return $this->getDocument('upgrades', $collection); } catch (\Throwable) { - return null; + return; } }); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 87c461f78..b154539aa 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -75,7 +75,6 @@ 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(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); $this->assertEquals(true, static::getDatabase()->create()); } @@ -317,7 +316,6 @@ public function testVirtualRelationsAttributes(): void /** * Success for later test update */ - $doc = static::getDatabase()->createDocument('v1', new Document([ '$id' => 'man', '$permissions' => [ @@ -744,7 +742,8 @@ public function testPreserveDatesUpdate(): void $newDate = '2000-01-01T10:00:00.000+00:00'; $doc1->setAttribute('$updatedAt', $newDate); - static::getDatabase()->updateDocument('preserve_update_dates', 'doc1', $doc1); + $doc1 = static::getDatabase()->updateDocument('preserve_update_dates', 'doc1', $doc1); + $this->assertEquals($newDate, $doc1->getAttribute('$updatedAt')); $doc1 = static::getDatabase()->getDocument('preserve_update_dates', 'doc1'); $this->assertEquals($newDate, $doc1->getAttribute('$updatedAt')); @@ -14983,7 +14982,6 @@ public function testIsolationModes(): void /** * Table */ - $tenant1 = 1; $tenant2 = 2; From 81516d51675cf0a5c5f75cd539beca32c16e6489 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 18:51:44 +1200 Subject: [PATCH 013/256] Add mirror containers to compose stack --- docker-compose.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 4d10e8cb6..90f781c7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,16 @@ services: - "8701:3306" environment: - MYSQL_ROOT_PASSWORD=password + + mariadb-mirror: + image: mariadb:10.11 + container_name: utopia-mariadb-mirror + networks: + - database + ports: + - "8704:3306" + environment: + - MYSQL_ROOT_PASSWORD=password mongo: image: mongo:5.0 @@ -72,6 +82,22 @@ services: cap_add: - SYS_NICE + mysql-mirror: + image: mysql:8.0.33 + container_name: utopia-mysql-mirror + networks: + - database + ports: + - "8705:3307" + environment: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: default + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_TCP_PORT: 3307 + cap_add: + - SYS_NICE + redis: image: redis:6.0-alpine container_name: utopia-redis From f181c7efebf18a1d43f0717d20ca38383dc0ff43 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 19:45:07 +1200 Subject: [PATCH 014/256] Fix return type for setters --- src/Database/Database.php | 2 +- src/Database/Mirror.php | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 868131d07..c56d811ef 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1478,7 +1478,7 @@ protected function updateAttributeMeta(string $collection, string $id, callable $updateCallback($attributes[$index], $collection, $index); // Save - $collection->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN); + $collection->setAttribute('attributes', $attributes); $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 659b8fc54..79aa02442 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -96,37 +96,51 @@ protected function delegate(string $method, array $args = []): mixed public function setDatabase(string $name): static { - return $this->delegate('setDatabase', [$name]); + $this->delegate('setDatabase', [$name]); + + return $this; } public function setNamespace(string $namespace): static { - return $this->delegate('setNamespace', [$namespace]); + $this->delegate('setNamespace', [$namespace]); + + return $this; } public function setSharedTables(bool $sharedTables): static { - return $this->delegate('setSharedTables', [$sharedTables]); + $this->delegate('setSharedTables', [$sharedTables]); + + return $this; } public function setTenant(?int $tenant): static { - return $this->delegate('setTenant', [$tenant]); + $this->delegate('setTenant', [$tenant]); + + return $this; } public function setPreserveDates(bool $preserve): static { - return $this->delegate('setPreserveDates', [$preserve]); + $this->delegate('setPreserveDates', [$preserve]); + + return $this; } public function enableValidation(): static { - return $this->delegate('enableValidation'); + $this->delegate('enableValidation'); + + return $this; } public function disableValidation(): static { - return $this->delegate('disableValidation'); + $this->delegate('disableValidation'); + + return $this; } public function exists(?string $database = null, ?string $collection = null): bool From a4c41095ba7b8659c8924f7382a0e6a62fc15a51 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 19:46:09 +1200 Subject: [PATCH 015/256] Fix events firing for destination db --- src/Database/Mirror.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 79aa02442..cba3a3319 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -143,6 +143,23 @@ public function disableValidation(): static return $this; } + public function on(string $event, string $name, callable $callback): static + { + $this->source->on($event, $name, $callback); + + return $this; + } + + protected function trigger(string $event, mixed $args = null): void + { + $this->source->trigger($event, $args); + } + + public function silent(callable $callback, array $listeners = null): mixed + { + return $this->source->silent($callback, $listeners); + } + public function exists(?string $database = null, ?string $collection = null): bool { return $this->delegate('exists', [$database, $collection]); From 944aed63151c97351f4ca9dfd12573b0d408b342 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 19:46:54 +1200 Subject: [PATCH 016/256] Fix timestamp conflicts --- src/Database/Mirror.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index cba3a3319..be350d296 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -160,6 +160,11 @@ public function silent(callable $callback, array $listeners = null): mixed return $this->source->silent($callback, $listeners); } + public function withRequestTimestamp(?\DateTime $requestTimestamp, callable $callback): mixed + { + return $this->delegate('withRequestTimestamp', [$requestTimestamp, $callback]); + } + public function exists(?string $database = null, ?string $collection = null): bool { return $this->delegate('exists', [$database, $collection]); From fd22e9a9346008f94937cf1a3c3bc454b5df5a35 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 20:27:51 +1200 Subject: [PATCH 017/256] Fix setters on parents --- src/Database/Adapter/MariaDB.php | 3 +-- src/Database/Database.php | 2 ++ src/Database/Mirror.php | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 4086b90f9..369cadfa1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -841,8 +841,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->bindValue(':_tenant', $this->tenant); } } - - + $stmt->execute(); $document['$internalId'] = $this->getDocument($collection, $document->getId(), [Query::select(['$internalId'])])->getInternalId(); diff --git a/src/Database/Database.php b/src/Database/Database.php index c56d811ef..35f446c9a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3109,9 +3109,11 @@ public function createDocument(string $collection, Document $document): Document if ($this->resolveRelationships) { $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); } + $document = $this->decode($collection, $document); $this->trigger(self::EVENT_DOCUMENT_CREATE, $document); + return $document; } diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index be350d296..bd7704f8e 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -126,6 +126,8 @@ public function setPreserveDates(bool $preserve): static { $this->delegate('setPreserveDates', [$preserve]); + $this->preserveDates = $preserve; + return $this; } @@ -133,6 +135,8 @@ public function enableValidation(): static { $this->delegate('enableValidation'); + $this->validate = true; + return $this; } @@ -140,6 +144,8 @@ public function disableValidation(): static { $this->delegate('disableValidation'); + $this->validate = false; + return $this; } From d6c7adfe16571d88bdc43fe26bcbda2faf0f81d9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 20:29:11 +1200 Subject: [PATCH 018/256] Fix timestamps --- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Mirror.php | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 369cadfa1..e2d552457 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -841,7 +841,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->bindValue(':_tenant', $this->tenant); } } - + $stmt->execute(); $document['$internalId'] = $this->getDocument($collection, $document->getId(), [Query::select(['$internalId'])])->getInternalId(); diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index bd7704f8e..a6ae8b84e 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -259,6 +259,14 @@ public function deleteIndex(string $collection, string $id): bool public function createDocument(string $collection, Document $document): Document { + $time = DateTime::now(); + + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + + $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + $document = $this->source->createDocument($collection, $document); if ( @@ -300,6 +308,16 @@ public function createDocuments( array $documents, int $batchSize = self::INSERT_BATCH_SIZE ): array { + $time = DateTime::now(); + + foreach ($documents as $document) { + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + + $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + } + $documents = $this->source->createDocuments($collection, $documents, $batchSize); if ( @@ -344,6 +362,14 @@ public function createDocuments( public function updateDocument(string $collection, string $id, Document $document): Document { + $time = DateTime::now(); + + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + + $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + $document = $this->source->updateDocument($collection, $id, $document); if ( @@ -385,6 +411,16 @@ public function updateDocuments( array $documents, int $batchSize = self::INSERT_BATCH_SIZE ): array { + $time = DateTime::now(); + + foreach ($documents as $document) { + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + + $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + } + $documents = $this->source->updateDocuments($collection, $documents, $batchSize); if ( From ba308bd5c0932997de8e17e8855c41ac8b8dc776 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 7 Jun 2024 20:31:57 +1200 Subject: [PATCH 019/256] Add document crud tests --- tests/e2e/Adapter/MirrorTest.php | 159 ++++++++++++++++++++++++++++--- 1 file changed, 147 insertions(+), 12 deletions(-) diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php index 258241945..8a0bef837 100644 --- a/tests/e2e/Adapter/MirrorTest.php +++ b/tests/e2e/Adapter/MirrorTest.php @@ -8,10 +8,13 @@ use Utopia\Cache\Cache; use Utopia\Database\Adapter\MariaDB; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Exception; +use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Limit; +use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Mirror; @@ -80,7 +83,7 @@ protected static function getAdapterName(): string * @throws Exception * @throws \RedisException */ - public function testGetSource(): void + public function testGetMirrorSource(): void { $database = self::getDatabase(); $source = $database->getSource(); @@ -92,7 +95,7 @@ public function testGetSource(): void * @throws Exception * @throws \RedisException */ - public function testGetDestination(): void + public function testGetMirrorDestination(): void { $database = self::getDatabase(); $destination = $database->getDestination(); @@ -106,15 +109,15 @@ public function testGetDestination(): void * @throws Exception * @throws \RedisException */ - public function testCreateCollection(): void + public function testCreateMirroredCollection(): void { $database = self::getDatabase(); - $database->createCollection('testCreateCollection'); + $database->createCollection('testCreateMirroredCollection'); // Assert collection exists in both databases - $this->assertFalse($database->getSource()->getCollection('testCreateCollection')->isEmpty()); - $this->assertFalse($database->getDestination()->getCollection('testCreateCollection')->isEmpty()); + $this->assertFalse($database->getSource()->getCollection('testCreateMirroredCollection')->isEmpty()); + $this->assertFalse($database->getDestination()->getCollection('testCreateMirroredCollection')->isEmpty()); } /** @@ -124,18 +127,18 @@ public function testCreateCollection(): void * @throws Conflict * @throws Exception */ - public function testUpdateCollection(): void + public function testUpdateMirroredCollection(): void { $database = self::getDatabase(); - $database->createCollection('testUpdateCollection', permissions: [ + $database->createCollection('testUpdateMirroredCollection', permissions: [ Permission::read(Role::any()), ]); - $collection = $database->getCollection('testUpdateCollection'); + $collection = $database->getCollection('testUpdateMirroredCollection'); $database->updateCollection( - 'testUpdateCollection', + 'testUpdateMirroredCollection', [ Permission::read(Role::users()), ], @@ -145,12 +148,144 @@ public function testUpdateCollection(): void // Asset both databases have updated the collection $this->assertEquals( [Permission::read(Role::users())], - $database->getSource()->getCollection('testUpdateCollection')->getPermissions() + $database->getSource()->getCollection('testUpdateMirroredCollection')->getPermissions() ); $this->assertEquals( [Permission::read(Role::users())], - $database->getDestination()->getCollection('testUpdateCollection')->getPermissions() + $database->getDestination()->getCollection('testUpdateMirroredCollection')->getPermissions() ); } + + public function testDeleteMirroredCollection(): void + { + $database = self::getDatabase(); + + $database->createCollection('testDeleteMirroredCollection'); + + $database->deleteCollection('testDeleteMirroredCollection'); + + // Assert collection is deleted in both databases + $this->assertTrue($database->getSource()->getCollection('testDeleteMirroredCollection')->isEmpty()); + $this->assertTrue($database->getDestination()->getCollection('testDeleteMirroredCollection')->isEmpty()); + } + + /** + * @throws Authorization + * @throws Duplicate + * @throws \RedisException + * @throws Limit + * @throws Structure + * @throws Exception + */ + public function testCreateMirroredDocument(): void + { + $database = self::getDatabase(); + + $database->createCollection('testCreateMirroredDocument', attributes: [ + new Document([ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'required' => true, + 'size' => Database::LENGTH_KEY, + ]), + ], permissions: [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], documentSecurity: false); + + $document = $database->createDocument('testCreateMirroredDocument', new Document([ + 'name' => 'Jake', + '$permissions' => [] + ])); + + // Assert document is created in both databases + $this->assertEquals( + $document, + $database->getSource()->getDocument('testCreateMirroredDocument', $document->getId()) + ); + + $this->assertEquals( + $document, + $database->getDestination()->getDocument('testCreateMirroredDocument', $document->getId()) + ); + } + + /** + * @throws Authorization + * @throws Duplicate + * @throws \RedisException + * @throws Conflict + * @throws Limit + * @throws Structure + * @throws Exception + */ + public function testUpdateMirroredDocument(): void + { + $database = self::getDatabase(); + + $database->createCollection('testUpdateMirroredDocument', attributes: [ + new Document([ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'required' => true, + 'size' => Database::LENGTH_KEY, + ]), + ], permissions: [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + ], documentSecurity: false); + + $document = $database->createDocument('testUpdateMirroredDocument', new Document([ + 'name' => 'Jake', + '$permissions' => [] + ])); + + $document = $database->updateDocument( + 'testUpdateMirroredDocument', + $document->getId(), + $document->setAttribute('name', 'John') + ); + + // Assert document is updated in both databases + $this->assertEquals( + $document, + $database->getSource()->getDocument('testUpdateMirroredDocument', $document->getId()) + ); + + $this->assertEquals( + $document, + $database->getDestination()->getDocument('testUpdateMirroredDocument', $document->getId()) + ); + } + + public function testDeleteMirroredDocument(): void + { + $database = self::getDatabase(); + + $database->createCollection('testDeleteMirroredDocument', attributes: [ + new Document([ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'required' => true, + 'size' => Database::LENGTH_KEY, + ]), + ], permissions: [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()), + ], documentSecurity: false); + + $document = $database->createDocument('testDeleteMirroredDocument', new Document([ + 'name' => 'Jake', + '$permissions' => [] + ])); + + $database->deleteDocument('testDeleteMirroredDocument', $document->getId()); + + // Assert document is deleted in both databases + $this->assertTrue($database->getSource()->getDocument('testDeleteMirroredDocument', $document->getId())->isEmpty()); + $this->assertTrue($database->getDestination()->getDocument('testDeleteMirroredDocument', $document->getId())->isEmpty()); + } } From e1c484d24397c0e413062bc9c0c905b5ef6fceaa Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 10 Jun 2024 18:30:18 +1200 Subject: [PATCH 020/256] Revert "Fix timestamps" This reverts commit d6c7adfe16571d88bdc43fe26bcbda2faf0f81d9. --- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Mirror.php | 36 -------------------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e2d552457..369cadfa1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -841,7 +841,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->bindValue(':_tenant', $this->tenant); } } - + $stmt->execute(); $document['$internalId'] = $this->getDocument($collection, $document->getId(), [Query::select(['$internalId'])])->getInternalId(); diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index a6ae8b84e..bd7704f8e 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -259,14 +259,6 @@ public function deleteIndex(string $collection, string $id): bool public function createDocument(string $collection, Document $document): Document { - $time = DateTime::now(); - - $createdAt = $document->getCreatedAt(); - $updatedAt = $document->getUpdatedAt(); - - $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - $document = $this->source->createDocument($collection, $document); if ( @@ -308,16 +300,6 @@ public function createDocuments( array $documents, int $batchSize = self::INSERT_BATCH_SIZE ): array { - $time = DateTime::now(); - - foreach ($documents as $document) { - $createdAt = $document->getCreatedAt(); - $updatedAt = $document->getUpdatedAt(); - - $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - } - $documents = $this->source->createDocuments($collection, $documents, $batchSize); if ( @@ -362,14 +344,6 @@ public function createDocuments( public function updateDocument(string $collection, string $id, Document $document): Document { - $time = DateTime::now(); - - $createdAt = $document->getCreatedAt(); - $updatedAt = $document->getUpdatedAt(); - - $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - $document = $this->source->updateDocument($collection, $id, $document); if ( @@ -411,16 +385,6 @@ public function updateDocuments( array $documents, int $batchSize = self::INSERT_BATCH_SIZE ): array { - $time = DateTime::now(); - - foreach ($documents as $document) { - $createdAt = $document->getCreatedAt(); - $updatedAt = $document->getUpdatedAt(); - - $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - } - $documents = $this->source->updateDocuments($collection, $documents, $batchSize); if ( From de90a3fa61b0dfeaa01269b2bc90ba6e21d338cc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 10 Jun 2024 21:16:58 +1200 Subject: [PATCH 021/256] Add mirror to tests --- .github/workflows/tests.yml | 1 + src/Database/Adapter/MariaDB.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b3dabf856..7b9f672af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,6 +77,7 @@ jobs: Postgres, SQLite, MongoDB, + Mirror, SharedTables/MariaDB, SharedTables/MySQL, SharedTables/Postgres, diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 369cadfa1..e2d552457 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -841,7 +841,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->bindValue(':_tenant', $this->tenant); } } - + $stmt->execute(); $document['$internalId'] = $this->getDocument($collection, $document->getId(), [Query::select(['$internalId'])])->getInternalId(); From 5da0ff69e867ab01bfe9c6cc5bb4a2c419691ae7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 12 Jun 2024 13:45:05 +1200 Subject: [PATCH 022/256] Add for each filter function --- src/Database/Mirror.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index bd7704f8e..dccd276b2 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -591,6 +591,17 @@ public function createUpgrades(): void } } + /** + * @param callable(Filter): void $callback + * @return void + */ + public function forEachFilter(callable $callback): void + { + foreach ($this->writeFilters as $filter) { + $callback($filter); + } + } + /** * @throws Exception */ From 01468e56a88713c621949eff6f87769cbb086d92 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 12 Jun 2024 14:06:17 +1200 Subject: [PATCH 023/256] Expose filters directly --- src/Database/Mirror.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index dccd276b2..82376df29 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -63,6 +63,14 @@ public function getDestination(): ?Database return $this->destination; } + /** + * @return array + */ + public function getWriteFilters(): array + { + return $this->writeFilters; + } + /** * @param callable(string, \Throwable): void $callback * @return void @@ -591,17 +599,6 @@ public function createUpgrades(): void } } - /** - * @param callable(Filter): void $callback - * @return void - */ - public function forEachFilter(callable $callback): void - { - foreach ($this->writeFilters as $filter) { - $callback($filter); - } - } - /** * @throws Exception */ From f99148a158148186d18c78bdc6b0954ce58dfd1c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 12 Jun 2024 14:52:43 +1200 Subject: [PATCH 024/256] Add default method stubs --- src/Database/Mirror.php | 248 +++++++++++++++++++++++++++++- src/Database/Mirroring/Filter.php | 176 ++++++++++++++++++++- 2 files changed, 411 insertions(+), 13 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 82376df29..38668922c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -209,6 +209,15 @@ public function createCollection(string $id, array $attributes = [], array $inde } try { + foreach ($this->writeFilters as $filter) { + $result = $filter->onCreateCollection( + source: $this->source, + destination: $this->destination, + collectionId: $id, + collection: $result, + ); + } + $this->destination->createCollection( $id, $attributes, @@ -232,37 +241,262 @@ public function createCollection(string $id, array $attributes = [], array $inde public function updateCollection(string $id, array $permissions, bool $documentSecurity): Document { - return $this->delegate('updateCollection', [$id, $permissions, $documentSecurity]); + $result = $this->source->updateCollection($id, $permissions, $documentSecurity); + + if ($this->destination === null) { + return $result; + } + + try { + foreach ($this->writeFilters as $filter) { + $result = $filter->onUpdateCollection( + source: $this->source, + destination: $this->destination, + collectionId: $id, + collection: $result, + ); + } + + $this->destination->updateCollection($id, $permissions, $documentSecurity); + } catch (\Throwable $err) { + $this->logError('updateCollection', $err); + } + + return $result; } public function deleteCollection(string $id): bool { - return $this->delegate('deleteCollection', [$id]); + $result = $this->source->deleteCollection($id); + + if ($this->destination === null) { + return $result; + } + + try { + $this->destination->deleteCollection($id); + + foreach ($this->writeFilters as $filter) { + $filter->onDeleteCollection( + source: $this->source, + destination: $this->destination, + collectionId: $id, + ); + } + } catch (\Throwable $err) { + $this->logError('deleteCollection', $err); + } + + return $result; } public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, string $format = null, array $formatOptions = [], array $filters = []): bool { - return $this->delegate('createAttribute', [$collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters]); + $result = $this->source->createAttribute( + $collection, + $id, + $type, + $size, + $required, + $default, + $signed, + $array, + $format, + $formatOptions, + $filters + ); + + if ($this->destination === null) { + return $result; + } + + try { + $document = new Document([ + '$id' => $id, + 'type' => $type, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'signed' => $signed, + 'array' => $array, + 'format' => $format, + 'formatOptions' => $formatOptions, + 'filters' => $filters, + ]); + + foreach ($this->writeFilters as $filter) { + $document = $filter->onCreateAttribute( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + attributeId: $id, + attribute: $document, + ); + } + + $result = $this->destination->createAttribute( + $collection, + $document->getId(), + $document->getAttribute('type'), + $document->getAttribute('size'), + $document->getAttribute('required'), + $document->getAttribute('default'), + $document->getAttribute('signed'), + $document->getAttribute('array'), + $document->getAttribute('format'), + $document->getAttribute('formatOptions'), + $document->getAttribute('filters'), + ); + } catch (\Throwable $err) { + $this->logError('createAttribute', $err); + } + + return $result; } public function updateAttribute(string $collection, string $id, string $type = null, int $size = null, bool $required = null, mixed $default = null, bool $signed = null, bool $array = null, string $format = null, ?array $formatOptions = null, ?array $filters = null): Document { - return $this->delegate('updateAttribute', [$collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters]); + $document = $this->source->updateAttribute( + $collection, + $id, + $type, + $size, + $required, + $default, + $signed, + $array, + $format, + $formatOptions, + $filters + ); + + if ($this->destination === null) { + return $document; + } + + try { + foreach ($this->writeFilters as $filter) { + $document = $filter->onUpdateAttribute( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + attributeId: $id, + attribute: $document, + ); + } + + $this->destination->updateAttribute( + $collection, + $id, + $document->getAttribute('type'), + $document->getAttribute('size'), + $document->getAttribute('required'), + $document->getAttribute('default'), + $document->getAttribute('signed'), + $document->getAttribute('array'), + $document->getAttribute('format'), + $document->getAttribute('formatOptions'), + $document->getAttribute('filters') + ); + } catch (\Throwable $err) { + $this->logError('updateAttribute', $err); + } + + return $document; } public function deleteAttribute(string $collection, string $id): bool { - return $this->delegate('deleteAttribute', [$collection, $id]); + $result = $this->source->deleteAttribute($collection, $id); + + if ($this->destination === null) { + return $result; + } + + try { + foreach ($this->writeFilters as $filter) { + $filter->onDeleteAttribute( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + attributeId: $id, + ); + } + + $this->destination->deleteAttribute($collection, $id); + } catch (\Throwable $err) { + $this->logError('deleteAttribute', $err); + } + + return $result; } public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool { - return $this->delegate('createIndex', [$collection, $id, $type, $attributes, $lengths, $orders]); + $result = $this->source->createIndex($collection, $id, $type, $attributes, $lengths, $orders); + + if ($this->destination === null) { + return $result; + } + + try { + $document = new Document([ + '$id' => $id, + 'type' => $type, + 'attributes' => $attributes, + 'lengths' => $lengths, + 'orders' => $orders, + ]); + + foreach ($this->writeFilters as $filter) { + $document = $filter->onCreateIndex( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + indexId: $id, + index: $document, + ); + } + + $result = $this->destination->createIndex( + $collection, + $document->getId(), + $document->getAttribute('type'), + $document->getAttribute('attributes'), + $document->getAttribute('lengths'), + $document->getAttribute('orders') + ); + } catch (\Throwable $err) { + $this->logError('createIndex', $err); + } + + return $result; } public function deleteIndex(string $collection, string $id): bool { - return $this->delegate('deleteIndex', [$collection, $id]); + $result = $this->source->deleteIndex($collection, $id); + + if ($this->destination === null) { + return $result; + } + + try { + $this->destination->deleteIndex($collection, $id); + + foreach ($this->writeFilters as $filter) { + $filter->onDeleteIndex( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + indexId: $id, + ); + } + } catch (\Throwable $err) { + $this->logError('deleteIndex', $err); + } + + return $result; } public function createDocument(string $collection, Document $document): Document diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index f247b9220..2464c4d34 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -7,6 +7,164 @@ abstract class Filter { + /** + * Called before collection is created in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param Document $collection + * @return Document + */ + public function onCreateCollection( + Database $source, + Database $destination, + string $collectionId, + Document $collection, + ): Document { + return $collection; + } + + /** + * Called before collection is updated in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param Document $collection + * @return Document + */ + public function onUpdateCollection( + Database $source, + Database $destination, + string $collectionId, + Document $collection, + ): Document { + return $collection; + } + + /** + * Called after collection is deleted in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @return void + */ + public function onDeleteCollection( + Database $source, + Database $destination, + string $collectionId, + ): void { + return; + } + + /** + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $attributeId + * @param Document $attribute + * @return Document + */ + public function onCreateAttribute( + Database $source, + Database $destination, + string $collectionId, + string $attributeId, + Document $attribute, + ): Document { + return $attribute; + } + + /** + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $attributeId + * @param Document $attribute + * @return Document + */ + public function onUpdateAttribute( + Database $source, + Database $destination, + string $collectionId, + string $attributeId, + Document $attribute, + ): Document { + return $attribute; + } + + /** + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $attributeId + * @return void + */ + public function onDeleteAttribute( + Database $source, + Database $destination, + string $collectionId, + string $attributeId, + ): void { + return; + } + + // Indexes + + /** + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $indexId + * @param Document $index + * @return Document + */ + public function onCreateIndex( + Database $source, + Database $destination, + string $collectionId, + string $indexId, + Document $index, + ): Document { + return $index; + } + + /** + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $indexId + * @param Document $index + * @return Document + */ + public function onUpdateIndex( + Database $source, + Database $destination, + string $collectionId, + string $indexId, + Document $index, + ): Document { + return $index; + } + + /** + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $indexId + * @return void + */ + public function onDeleteIndex( + Database $source, + Database $destination, + string $collectionId, + string $indexId, + ): void { + return; + } + /** * Called before document is created in the destination database * @@ -16,12 +174,14 @@ abstract class Filter * @param Document $document * @return Document */ - abstract public function onCreateDocument( + public function onCreateDocument( Database $source, Database $destination, string $collectionId, Document $document, - ): Document; + ): Document { + return $document; + } /** @@ -33,12 +193,14 @@ abstract public function onCreateDocument( * @param Document $document * @return Document */ - abstract public function onUpdateDocument( + public function onUpdateDocument( Database $source, Database $destination, string $collectionId, Document $document, - ): Document; + ): Document { + return $document; + } /** * Called after document is deleted in the destination database @@ -49,10 +211,12 @@ abstract public function onUpdateDocument( * @param string $documentId * @return void */ - abstract public function onDeleteDocument( + public function onDeleteDocument( Database $source, Database $destination, string $collectionId, string $documentId, - ): void; + ): void { + return; + } } From 238e4a072604e2e32f47c712b8efa9c973e2e8b9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 12 Jun 2024 15:17:42 +1200 Subject: [PATCH 025/256] Allow hooking after even as well as before --- src/Database/Mirror.php | 73 +++++++++++++++++++++++------ src/Database/Mirroring/Filter.php | 78 ++++++++++++++++++++++++------- 2 files changed, 120 insertions(+), 31 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 38668922c..2911a0231 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -210,7 +210,7 @@ public function createCollection(string $id, array $attributes = [], array $inde try { foreach ($this->writeFilters as $filter) { - $result = $filter->onCreateCollection( + $result = $filter->beforeCreateCollection( source: $this->source, destination: $this->destination, collectionId: $id, @@ -249,7 +249,7 @@ public function updateCollection(string $id, array $permissions, bool $documentS try { foreach ($this->writeFilters as $filter) { - $result = $filter->onUpdateCollection( + $result = $filter->beforeUpdateCollection( source: $this->source, destination: $this->destination, collectionId: $id, @@ -277,7 +277,7 @@ public function deleteCollection(string $id): bool $this->destination->deleteCollection($id); foreach ($this->writeFilters as $filter) { - $filter->onDeleteCollection( + $filter->beforeDeleteCollection( source: $this->source, destination: $this->destination, collectionId: $id, @@ -325,7 +325,7 @@ public function createAttribute(string $collection, string $id, string $type, in ]); foreach ($this->writeFilters as $filter) { - $document = $filter->onCreateAttribute( + $document = $filter->beforeCreateAttribute( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -334,7 +334,7 @@ public function createAttribute(string $collection, string $id, string $type, in ); } - $result = $this->destination->createAttribute( + $result = $this->destination->createAttribute( $collection, $document->getId(), $document->getAttribute('type'), @@ -376,7 +376,7 @@ public function updateAttribute(string $collection, string $id, string $type = n try { foreach ($this->writeFilters as $filter) { - $document = $filter->onUpdateAttribute( + $document = $filter->beforeUpdateAttribute( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -415,7 +415,7 @@ public function deleteAttribute(string $collection, string $id): bool try { foreach ($this->writeFilters as $filter) { - $filter->onDeleteAttribute( + $filter->beforeDeleteAttribute( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -449,7 +449,7 @@ public function createIndex(string $collection, string $id, string $type, array ]); foreach ($this->writeFilters as $filter) { - $document = $filter->onCreateIndex( + $document = $filter->beforeCreateIndex( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -485,7 +485,7 @@ public function deleteIndex(string $collection, string $id): bool $this->destination->deleteIndex($collection, $id); foreach ($this->writeFilters as $filter) { - $filter->onDeleteIndex( + $filter->beforeDeleteIndex( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -519,7 +519,7 @@ public function createDocument(string $collection, Document $document): Document $clone = clone $document; foreach ($this->writeFilters as $filter) { - $clone = $filter->onCreateDocument( + $clone = $filter->beforeCreateDocument( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -528,8 +528,17 @@ public function createDocument(string $collection, Document $document): Document } $this->destination->setPreserveDates(true); - $this->destination->createDocument($collection, $clone); + $document = $this->destination->createDocument($collection, $clone); $this->destination->setPreserveDates(false); + + foreach ($this->writeFilters as $filter) { + $filter->afterCreateDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + document: $clone, + ); + } } catch (\Throwable $err) { $this->logError('createDocument', $err); } @@ -563,7 +572,7 @@ public function createDocuments( $clone = clone $document; foreach ($this->writeFilters as $filter) { - $clone = $filter->onCreateDocument( + $clone = $filter->beforeCreateDocument( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -577,6 +586,18 @@ public function createDocuments( $this->destination->setPreserveDates(true); $this->destination->createDocuments($collection, $clones, $batchSize); $this->destination->setPreserveDates(false); + + foreach ($clones as $clone) { + foreach ($this->writeFilters as $filter) { + $filter->afterCreateDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + document: $clone, + ); + } + } + } catch (\Throwable $err) { $this->logError('createDocuments', $err); } @@ -604,7 +625,7 @@ public function updateDocument(string $collection, string $id, Document $documen $clone = clone $document; foreach ($this->writeFilters as $filter) { - $clone = $filter->onUpdateDocument( + $clone = $filter->beforeUpdateDocument( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -615,6 +636,15 @@ public function updateDocument(string $collection, string $id, Document $documen $this->destination->setPreserveDates(true); $this->destination->updateDocument($collection, $id, $clone); $this->destination->setPreserveDates(false); + + foreach ($this->writeFilters as $filter) { + $filter->afterUpdateDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + document: $clone, + ); + } } catch (\Throwable $err) { $this->logError('updateDocument', $err); } @@ -648,7 +678,7 @@ public function updateDocuments( $clone = clone $document; foreach ($this->writeFilters as $filter) { - $clone = $filter->onUpdateDocument( + $clone = $filter->beforeUpdateDocument( source: $this->source, destination: $this->destination, collectionId: $collection, @@ -662,6 +692,17 @@ public function updateDocuments( $this->destination->setPreserveDates(true); $this->destination->updateDocuments($collection, $clones, $batchSize); $this->destination->setPreserveDates(false); + + foreach ($clones as $clone) { + foreach ($this->writeFilters as $filter) { + $filter->afterUpdateDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + document: $clone, + ); + } + } } catch (\Throwable $err) { $this->logError('updateDocuments', $err); } @@ -686,10 +727,12 @@ public function deleteDocument(string $collection, string $id): bool } try { + + $this->destination->deleteDocument($collection, $id); foreach ($this->writeFilters as $filter) { - $filter->onDeleteDocument( + $filter->afterDeleteDocument( source: $this->source, destination: $this->destination, collectionId: $collection, diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index 2464c4d34..ec9c30379 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -16,7 +16,7 @@ abstract class Filter * @param Document $collection * @return Document */ - public function onCreateCollection( + public function beforeCreateCollection( Database $source, Database $destination, string $collectionId, @@ -34,7 +34,7 @@ public function onCreateCollection( * @param Document $collection * @return Document */ - public function onUpdateCollection( + public function beforeUpdateCollection( Database $source, Database $destination, string $collectionId, @@ -51,12 +51,11 @@ public function onUpdateCollection( * @param string $collectionId * @return void */ - public function onDeleteCollection( + public function beforeDeleteCollection( Database $source, Database $destination, string $collectionId, ): void { - return; } /** @@ -67,7 +66,7 @@ public function onDeleteCollection( * @param Document $attribute * @return Document */ - public function onCreateAttribute( + public function beforeCreateAttribute( Database $source, Database $destination, string $collectionId, @@ -85,7 +84,7 @@ public function onCreateAttribute( * @param Document $attribute * @return Document */ - public function onUpdateAttribute( + public function beforeUpdateAttribute( Database $source, Database $destination, string $collectionId, @@ -102,13 +101,12 @@ public function onUpdateAttribute( * @param string $attributeId * @return void */ - public function onDeleteAttribute( + public function beforeDeleteAttribute( Database $source, Database $destination, string $collectionId, string $attributeId, ): void { - return; } // Indexes @@ -121,7 +119,7 @@ public function onDeleteAttribute( * @param Document $index * @return Document */ - public function onCreateIndex( + public function beforeCreateIndex( Database $source, Database $destination, string $collectionId, @@ -139,7 +137,7 @@ public function onCreateIndex( * @param Document $index * @return Document */ - public function onUpdateIndex( + public function beforeUpdateIndex( Database $source, Database $destination, string $collectionId, @@ -156,13 +154,12 @@ public function onUpdateIndex( * @param string $indexId * @return void */ - public function onDeleteIndex( + public function beforeDeleteIndex( Database $source, Database $destination, string $collectionId, string $indexId, ): void { - return; } /** @@ -174,7 +171,7 @@ public function onDeleteIndex( * @param Document $document * @return Document */ - public function onCreateDocument( + public function beforeCreateDocument( Database $source, Database $destination, string $collectionId, @@ -183,6 +180,22 @@ public function onCreateDocument( return $document; } + /** + * Called before document is created in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param Document $document + * @return void + */ + public function afterCreateDocument( + Database $source, + Database $destination, + string $collectionId, + Document $document, + ): void { + } /** * Called before document is updated in the destination database @@ -193,7 +206,7 @@ public function onCreateDocument( * @param Document $document * @return Document */ - public function onUpdateDocument( + public function beforeUpdateDocument( Database $source, Database $destination, string $collectionId, @@ -202,6 +215,40 @@ public function onUpdateDocument( return $document; } + /** + * Called after document is updated in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param Document $document + * @return void + */ + public function afterUpdateDocument( + Database $source, + Database $destination, + string $collectionId, + Document $document, + ): void { + } + + /** + * Called before document is deleted in the destination database + * + * @param Database $source + * @param Database $destination + * @param string $collectionId + * @param string $documentId + * @return void + */ + public function beforeDeleteDocument( + Database $source, + Database $destination, + string $collectionId, + string $documentId, + ): void { + } + /** * Called after document is deleted in the destination database * @@ -211,12 +258,11 @@ public function onUpdateDocument( * @param string $documentId * @return void */ - public function onDeleteDocument( + public function afterDeleteDocument( Database $source, Database $destination, string $collectionId, string $documentId, ): void { - return; } } From a358f8d37b10e3f90eed70b7706ce0587dba886f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 12 Jun 2024 15:55:56 +1200 Subject: [PATCH 026/256] Hook before delete --- src/Database/Mirror.php | 9 ++++++++- src/Database/Mirroring/Filter.php | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 2911a0231..f4a3ee8ad 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -727,7 +727,14 @@ public function deleteDocument(string $collection, string $id): bool } try { - + foreach ($this->writeFilters as $filter) { + $filter->beforeDeleteDocument( + source: $this->source, + destination: $this->destination, + collectionId: $collection, + documentId: $id, + ); + } $this->destination->deleteDocument($collection, $id); diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index ec9c30379..e239d5dca 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -181,7 +181,7 @@ public function beforeCreateDocument( } /** - * Called before document is created in the destination database + * Called after document is created in the destination database * * @param Database $source * @param Database $destination From 60b9f1081b59e127dae35165dcf28217b4637678 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 12 Jun 2024 20:27:40 +1200 Subject: [PATCH 027/256] Add init and shutdown hooks to filters --- src/Database/Mirror.php | 11 +++++++++++ src/Database/Mirroring/Filter.php | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index f4a3ee8ad..650d2e2a4 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -51,6 +51,17 @@ public function __construct( $this->source = $source; $this->destination = $destination; $this->writeFilters = $filters; + + foreach ($this->writeFilters as $filter) { + $filter->init($this->source, $this->destination); + } + } + + public function __destruct() + { + foreach ($this->writeFilters as $filter) { + $filter->shutdown($this->source, $this->destination); + } } public function getSource(): Database diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index e239d5dca..205fd70ac 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -7,6 +7,25 @@ abstract class Filter { + /** + * Called before any action is executed + * + * @param Database $source + * @param Database $destination + * @return void + */ + public function init( + Database $source, + Database $destination, + ): void { + } + + public function shutdown( + Database $source, + Database $destination, + ): void { + } + /** * Called before collection is created in the destination database * From 1ffa28554259e031e70d61d4da8f070093d4a74a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 2 Jul 2024 13:44:29 +1200 Subject: [PATCH 028/256] Ensure destination is nullable --- src/Database/Mirroring/Filter.php | 75 ++++++++++++++----------- src/Database/Validator/Query/Filter.php | 1 + 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index 205fd70ac..f291c7042 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -8,21 +8,28 @@ abstract class Filter { /** - * Called before any action is executed + * Called before any action is executed, when the filter is constructed. * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @return void */ public function init( Database $source, - Database $destination, + ?Database $destination, ): void { } + /** + * Called after all actions are executed, when the filter is destructed. + * + * @param Database $source + * @param ?Database $destination + * @return void + */ public function shutdown( Database $source, - Database $destination, + ?Database $destination, ): void { } @@ -30,14 +37,14 @@ public function shutdown( * Called before collection is created in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param Document $collection * @return Document */ public function beforeCreateCollection( Database $source, - Database $destination, + ?Database $destination, string $collectionId, Document $collection, ): Document { @@ -48,14 +55,14 @@ public function beforeCreateCollection( * Called before collection is updated in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param Document $collection * @return Document */ public function beforeUpdateCollection( Database $source, - Database $destination, + ?Database $destination, string $collectionId, Document $collection, ): Document { @@ -66,20 +73,20 @@ public function beforeUpdateCollection( * Called after collection is deleted in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @return void */ public function beforeDeleteCollection( Database $source, - Database $destination, + ?Database $destination, string $collectionId, ): void { } /** * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $attributeId * @param Document $attribute @@ -87,7 +94,7 @@ public function beforeDeleteCollection( */ public function beforeCreateAttribute( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $attributeId, Document $attribute, @@ -97,7 +104,7 @@ public function beforeCreateAttribute( /** * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $attributeId * @param Document $attribute @@ -105,7 +112,7 @@ public function beforeCreateAttribute( */ public function beforeUpdateAttribute( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $attributeId, Document $attribute, @@ -115,14 +122,14 @@ public function beforeUpdateAttribute( /** * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $attributeId * @return void */ public function beforeDeleteAttribute( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $attributeId, ): void { @@ -132,7 +139,7 @@ public function beforeDeleteAttribute( /** * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $indexId * @param Document $index @@ -140,7 +147,7 @@ public function beforeDeleteAttribute( */ public function beforeCreateIndex( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $indexId, Document $index, @@ -150,7 +157,7 @@ public function beforeCreateIndex( /** * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $indexId * @param Document $index @@ -158,7 +165,7 @@ public function beforeCreateIndex( */ public function beforeUpdateIndex( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $indexId, Document $index, @@ -168,14 +175,14 @@ public function beforeUpdateIndex( /** * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $indexId * @return void */ public function beforeDeleteIndex( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $indexId, ): void { @@ -185,14 +192,14 @@ public function beforeDeleteIndex( * Called before document is created in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param Document $document * @return Document */ public function beforeCreateDocument( Database $source, - Database $destination, + ?Database $destination, string $collectionId, Document $document, ): Document { @@ -203,14 +210,14 @@ public function beforeCreateDocument( * Called after document is created in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param Document $document * @return void */ public function afterCreateDocument( Database $source, - Database $destination, + ?Database $destination, string $collectionId, Document $document, ): void { @@ -220,14 +227,14 @@ public function afterCreateDocument( * Called before document is updated in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param Document $document * @return Document */ public function beforeUpdateDocument( Database $source, - Database $destination, + ?Database $destination, string $collectionId, Document $document, ): Document { @@ -238,14 +245,14 @@ public function beforeUpdateDocument( * Called after document is updated in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param Document $document * @return void */ public function afterUpdateDocument( Database $source, - Database $destination, + ?Database $destination, string $collectionId, Document $document, ): void { @@ -255,14 +262,14 @@ public function afterUpdateDocument( * Called before document is deleted in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $documentId * @return void */ public function beforeDeleteDocument( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $documentId, ): void { @@ -272,14 +279,14 @@ public function beforeDeleteDocument( * Called after document is deleted in the destination database * * @param Database $source - * @param Database $destination + * @param ?Database $destination * @param string $collectionId * @param string $documentId * @return void */ public function afterDeleteDocument( Database $source, - Database $destination, + ?Database $destination, string $collectionId, string $documentId, ): void { diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 635fa4732..f5027e6c5 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -67,6 +67,7 @@ protected function isValidAttribute(string $attribute): bool /** * @param string $attribute * @param array $values + * @param string $method * @return bool */ protected function isValidAttributeAndValues(string $attribute, array $values, string $method): bool From 28e41327874f849427a2b9c2f02d9d1a70d6b0cb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 2 Jul 2024 13:48:23 +1200 Subject: [PATCH 029/256] Nullable dest only for init and shutdown --- src/Database/Mirroring/Filter.php | 60 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index f291c7042..ce381dfec 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -37,14 +37,14 @@ public function shutdown( * Called before collection is created in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param Document $collection * @return Document */ public function beforeCreateCollection( Database $source, - ?Database $destination, + Database $destination, string $collectionId, Document $collection, ): Document { @@ -55,14 +55,14 @@ public function beforeCreateCollection( * Called before collection is updated in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param Document $collection * @return Document */ public function beforeUpdateCollection( Database $source, - ?Database $destination, + Database $destination, string $collectionId, Document $collection, ): Document { @@ -73,20 +73,20 @@ public function beforeUpdateCollection( * Called after collection is deleted in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @return void */ public function beforeDeleteCollection( Database $source, - ?Database $destination, + Database $destination, string $collectionId, ): void { } /** * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $attributeId * @param Document $attribute @@ -94,7 +94,7 @@ public function beforeDeleteCollection( */ public function beforeCreateAttribute( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $attributeId, Document $attribute, @@ -104,7 +104,7 @@ public function beforeCreateAttribute( /** * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $attributeId * @param Document $attribute @@ -112,7 +112,7 @@ public function beforeCreateAttribute( */ public function beforeUpdateAttribute( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $attributeId, Document $attribute, @@ -122,14 +122,14 @@ public function beforeUpdateAttribute( /** * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $attributeId * @return void */ public function beforeDeleteAttribute( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $attributeId, ): void { @@ -139,7 +139,7 @@ public function beforeDeleteAttribute( /** * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $indexId * @param Document $index @@ -147,7 +147,7 @@ public function beforeDeleteAttribute( */ public function beforeCreateIndex( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $indexId, Document $index, @@ -157,7 +157,7 @@ public function beforeCreateIndex( /** * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $indexId * @param Document $index @@ -165,7 +165,7 @@ public function beforeCreateIndex( */ public function beforeUpdateIndex( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $indexId, Document $index, @@ -175,14 +175,14 @@ public function beforeUpdateIndex( /** * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $indexId * @return void */ public function beforeDeleteIndex( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $indexId, ): void { @@ -192,14 +192,14 @@ public function beforeDeleteIndex( * Called before document is created in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param Document $document * @return Document */ public function beforeCreateDocument( Database $source, - ?Database $destination, + Database $destination, string $collectionId, Document $document, ): Document { @@ -210,14 +210,14 @@ public function beforeCreateDocument( * Called after document is created in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param Document $document * @return void */ public function afterCreateDocument( Database $source, - ?Database $destination, + Database $destination, string $collectionId, Document $document, ): void { @@ -227,14 +227,14 @@ public function afterCreateDocument( * Called before document is updated in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param Document $document * @return Document */ public function beforeUpdateDocument( Database $source, - ?Database $destination, + Database $destination, string $collectionId, Document $document, ): Document { @@ -245,14 +245,14 @@ public function beforeUpdateDocument( * Called after document is updated in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param Document $document * @return void */ public function afterUpdateDocument( Database $source, - ?Database $destination, + Database $destination, string $collectionId, Document $document, ): void { @@ -262,14 +262,14 @@ public function afterUpdateDocument( * Called before document is deleted in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $documentId * @return void */ public function beforeDeleteDocument( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $documentId, ): void { @@ -279,14 +279,14 @@ public function beforeDeleteDocument( * Called after document is deleted in the destination database * * @param Database $source - * @param ?Database $destination + * @param Database $destination * @param string $collectionId * @param string $documentId * @return void */ public function afterDeleteDocument( Database $source, - ?Database $destination, + Database $destination, string $collectionId, string $documentId, ): void { From 9801cf9eaa96a3c1005072b0874f612bd4926af3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 10 Jul 2024 15:51:34 +1200 Subject: [PATCH 030/256] Don't init filters on construction to allow for pre-config --- src/Database/Mirror.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 650d2e2a4..f4a3ee8ad 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -51,17 +51,6 @@ public function __construct( $this->source = $source; $this->destination = $destination; $this->writeFilters = $filters; - - foreach ($this->writeFilters as $filter) { - $filter->init($this->source, $this->destination); - } - } - - public function __destruct() - { - foreach ($this->writeFilters as $filter) { - $filter->shutdown($this->source, $this->destination); - } } public function getSource(): Database From ec87dfbe074acdf83c246df8b55e9a83b39aaf3e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 11 Jul 2024 17:38:31 +1200 Subject: [PATCH 031/256] Try read instead of check duplicate upgrades collection --- src/Database/Mirror.php | 96 +++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index f4a3ee8ad..ec0375e9c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -834,53 +834,55 @@ public function decreaseDocumentAttribute(string $collection, string $id, string */ public function createUpgrades(): void { - try { - $this->source->createCollection( - id: 'upgrades', - attributes: [ - new Document([ - '$id' => ID::custom('collectionId'), - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - 'default' => null, - 'format' => '' - ]), - new Document([ - '$id' => ID::custom('status'), - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'signed' => true, - 'array' => false, - 'filters' => [], - 'default' => null, - 'format' => '' - ]), - ], - indexes: [ - new Document([ - '$id' => ID::custom('_unique_collection'), - 'type' => Database::INDEX_UNIQUE, - 'attributes' => ['collectionId'], - 'lengths' => [Database::LENGTH_KEY], - 'orders' => [], - ]), - new Document([ - '$id' => ID::custom('_status_index'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['status'], - 'lengths' => [Database::LENGTH_KEY], - 'orders' => [Database::ORDER_ASC], - ]), - ], - ); - } catch (DuplicateException) { - // Ignore - } + $collection = $this->source->getCollection('upgrades'); + + if (!$collection->isEmpty()) { + return; + } + + $this->source->createCollection( + id: 'upgrades', + attributes: [ + new Document([ + '$id' => ID::custom('collectionId'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + 'default' => null, + 'format' => '' + ]), + new Document([ + '$id' => ID::custom('status'), + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + 'default' => null, + 'format' => '' + ]), + ], + indexes: [ + new Document([ + '$id' => ID::custom('_unique_collection'), + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['collectionId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [], + ]), + new Document([ + '$id' => ID::custom('_status_index'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['status'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ]), + ], + ); } /** From 6c86f496633e34b4f15ba479d7854013ff6131c4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 12 Jul 2024 00:28:23 +1200 Subject: [PATCH 032/256] Fetch internal ID for documents not created with it --- src/Database/Adapter/MariaDB.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e2d552457..ea245d02f 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -896,6 +896,8 @@ public function createDocuments(string $collection, array $documents, int $batch $name = $this->filter($collection); $batches = \array_chunk($documents, max(1, $batchSize)); + $internalIds = []; + foreach ($batches as $batch) { $bindIndex = 0; $batchKeys = []; @@ -908,7 +910,9 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); + if(!empty($document->getInternalId())) { + $internalIds[$document->getId()] = true; $attributes['_id'] = $document->getInternalId(); } @@ -1007,6 +1011,16 @@ public function createDocuments(string $collection, array $documents, int $batch throw $e; } + foreach ($documents as $document) { + if(!isset($internalIds[$document->getId()])) { + $document['$internalId'] = $this->getDocument( + $collection, + $document->getId(), + [Query::select(['$internalId'])] + )->getInternalId(); + } + } + return $documents; } From 89a0079fb68087f014098583ab56331b49b27961 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 22 Jul 2024 23:13:49 +1200 Subject: [PATCH 033/256] Ignore relationships that are virtual attributes --- src/Database/Adapter/MariaDB.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ea245d02f..cc6d337a7 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -91,6 +91,23 @@ public function createCollection(string $name, array $attributes = [], array $in $attribute->getAttribute('array', false) ); + // Ignore relationships with virtual attributes + if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $attribute->getAttribute('options', []); + $relationType = $options['relationType'] ?? null; + $twoWay = $options['twoWay'] ?? false; + $side = $options['side'] ?? null; + + if ( + $relationType === Database::RELATION_MANY_TO_MANY + || ($relationType === Database::RELATION_ONE_TO_ONE && !$twoWay && $side === Database::RELATION_SIDE_CHILD) + || ($relationType === Database::RELATION_ONE_TO_MANY && $side === Database::RELATION_SIDE_PARENT) + || ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_CHILD) + ) { + continue; + } + } + $attributeStrings[$key] = "`{$attrId}` {$attrType}, "; } @@ -894,7 +911,7 @@ public function createDocuments(string $collection, array $documents, int $batch $this->getPDO()->beginTransaction(); $name = $this->filter($collection); - $batches = \array_chunk($documents, max(1, $batchSize)); + $batches = \array_chunk($documents, \max(1, $batchSize)); $internalIds = []; From 68c988186f3cb6403ad5cf6cb6711523dc8e9e71 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 23 Jul 2024 17:25:50 +1200 Subject: [PATCH 034/256] Nullable filter params for noop hooks --- src/Database/Mirroring/Filter.php | 82 ++++++++++++++++--------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index ce381dfec..8bbc5779d 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -39,15 +39,15 @@ public function shutdown( * @param Database $source * @param Database $destination * @param string $collectionId - * @param Document $collection - * @return Document + * @param ?Document $collection + * @return ?Document */ public function beforeCreateCollection( Database $source, Database $destination, string $collectionId, - Document $collection, - ): Document { + ?Document $collection = null, + ): ?Document { return $collection; } @@ -57,15 +57,15 @@ public function beforeCreateCollection( * @param Database $source * @param Database $destination * @param string $collectionId - * @param Document $collection - * @return Document + * @param ?Document $collection + * @return ?Document */ public function beforeUpdateCollection( Database $source, Database $destination, string $collectionId, - Document $collection, - ): Document { + ?Document $collection = null, + ): ?Document { return $collection; } @@ -89,16 +89,16 @@ public function beforeDeleteCollection( * @param Database $destination * @param string $collectionId * @param string $attributeId - * @param Document $attribute - * @return Document + * @param ?Document $attribute + * @return ?Document */ public function beforeCreateAttribute( Database $source, Database $destination, string $collectionId, string $attributeId, - Document $attribute, - ): Document { + ?Document $attribute = null, + ): ?Document { return $attribute; } @@ -107,16 +107,16 @@ public function beforeCreateAttribute( * @param Database $destination * @param string $collectionId * @param string $attributeId - * @param Document $attribute - * @return Document + * @param ?Document $attribute + * @return ?Document */ public function beforeUpdateAttribute( Database $source, Database $destination, string $collectionId, string $attributeId, - Document $attribute, - ): Document { + ?Document $attribute = null, + ): ?Document { return $attribute; } @@ -142,16 +142,16 @@ public function beforeDeleteAttribute( * @param Database $destination * @param string $collectionId * @param string $indexId - * @param Document $index - * @return Document + * @param ?Document $index + * @return ?Document */ public function beforeCreateIndex( Database $source, Database $destination, string $collectionId, string $indexId, - Document $index, - ): Document { + ?Document $index = null, + ): ?Document { return $index; } @@ -160,16 +160,16 @@ public function beforeCreateIndex( * @param Database $destination * @param string $collectionId * @param string $indexId - * @param Document $index - * @return Document + * @param ?Document $index + * @return ?Document */ public function beforeUpdateIndex( Database $source, Database $destination, string $collectionId, string $indexId, - Document $index, - ): Document { + ?Document $index = null, + ): ?Document { return $index; } @@ -194,15 +194,15 @@ public function beforeDeleteIndex( * @param Database $source * @param Database $destination * @param string $collectionId - * @param Document $document - * @return Document + * @param ?Document $document + * @return ?Document */ public function beforeCreateDocument( Database $source, Database $destination, string $collectionId, - Document $document, - ): Document { + ?Document $document = null, + ): ?Document { return $document; } @@ -212,15 +212,16 @@ public function beforeCreateDocument( * @param Database $source * @param Database $destination * @param string $collectionId - * @param Document $document - * @return void + * @param ?Document $document + * @return ?Document */ public function afterCreateDocument( Database $source, Database $destination, string $collectionId, - Document $document, - ): void { + ?Document $document = null, + ): ?Document { + return $document; } /** @@ -229,15 +230,15 @@ public function afterCreateDocument( * @param Database $source * @param Database $destination * @param string $collectionId - * @param Document $document - * @return Document + * @param ?Document $document + * @return ?Document */ public function beforeUpdateDocument( Database $source, Database $destination, string $collectionId, - Document $document, - ): Document { + ?Document $document = null, + ): ?Document { return $document; } @@ -247,15 +248,16 @@ public function beforeUpdateDocument( * @param Database $source * @param Database $destination * @param string $collectionId - * @param Document $document - * @return void + * @param ?Document $document + * @return ?Document */ public function afterUpdateDocument( Database $source, Database $destination, string $collectionId, - Document $document, - ): void { + ?Document $document = null, + ): ?Document { + return $document; } /** From 6b0defbcb442507a5928afb7c40035f245e04d5c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 23 Jul 2024 17:38:47 +1200 Subject: [PATCH 035/256] Foce require document for document hooks --- src/Database/Mirroring/Filter.php | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Database/Mirroring/Filter.php b/src/Database/Mirroring/Filter.php index 8bbc5779d..159c6884c 100644 --- a/src/Database/Mirroring/Filter.php +++ b/src/Database/Mirroring/Filter.php @@ -194,15 +194,15 @@ public function beforeDeleteIndex( * @param Database $source * @param Database $destination * @param string $collectionId - * @param ?Document $document - * @return ?Document + * @param Document $document + * @return Document */ public function beforeCreateDocument( Database $source, Database $destination, string $collectionId, - ?Document $document = null, - ): ?Document { + Document $document, + ): Document { return $document; } @@ -212,15 +212,15 @@ public function beforeCreateDocument( * @param Database $source * @param Database $destination * @param string $collectionId - * @param ?Document $document - * @return ?Document + * @param Document $document + * @return Document */ public function afterCreateDocument( Database $source, Database $destination, string $collectionId, - ?Document $document = null, - ): ?Document { + Document $document, + ): Document { return $document; } @@ -230,15 +230,15 @@ public function afterCreateDocument( * @param Database $source * @param Database $destination * @param string $collectionId - * @param ?Document $document - * @return ?Document + * @param Document $document + * @return Document */ public function beforeUpdateDocument( Database $source, Database $destination, string $collectionId, - ?Document $document = null, - ): ?Document { + Document $document, + ): Document { return $document; } @@ -248,15 +248,15 @@ public function beforeUpdateDocument( * @param Database $source * @param Database $destination * @param string $collectionId - * @param ?Document $document - * @return ?Document + * @param Document $document + * @return Document */ public function afterUpdateDocument( Database $source, Database $destination, string $collectionId, - ?Document $document = null, - ): ?Document { + Document $document, + ): Document { return $document; } From 397aaa6ed4e7ee87042c51b2a1d900de13100679 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 15:46:06 +1200 Subject: [PATCH 036/256] Allow null tenant to allow global tables with single metadata doc --- src/Database/Adapter/MariaDB.php | 22 +++++++++++----------- src/Database/Adapter/Postgres.php | 22 +++++++++++----------- src/Database/Adapter/SQL.php | 4 ++-- src/Database/Adapter/SQLite.php | 10 +++++----- src/Database/Database.php | 26 +++++++++++++++----------- 5 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 28dad079a..3506221b7 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1046,7 +1046,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1123,7 +1123,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $removeQuery = $sql . $removeQuery; @@ -1210,7 +1210,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -1339,7 +1339,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1382,7 +1382,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant'; + $tenantQuery = ' AND _tenant IN (:_tenant, NULL)'; } $removeQuery .= "( @@ -1563,7 +1563,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql .= $sqlMax . $sqlMin; @@ -1605,7 +1605,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); @@ -1624,7 +1624,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); @@ -1767,7 +1767,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if ($this->sharedTables) { - $where[] = "table_main._tenant = :_tenant"; + $where[] = "table_main._tenant IN (:_tenant, NULL)"; } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; @@ -1893,7 +1893,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) } if ($this->sharedTables) { - $where[] = "table_main._tenant = :_tenant"; + $where[] = "table_main._tenant IN (:_tenant, NULL)"; } $sqlWhere = !empty($where) @@ -1963,7 +1963,7 @@ public function sum(string $collection, string $attribute, array $queries = [], } if ($this->sharedTables) { - $where[] = "table_main._tenant = :_tenant"; + $where[] = "table_main._tenant IN (:_tenant, NULL)"; } $sqlWhere = !empty($where) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 8686f4b2e..b6e98b069 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -977,7 +977,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1057,7 +1057,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $removeQuery = $sql . $removeQuery; @@ -1129,7 +1129,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -1253,7 +1253,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1296,7 +1296,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant'; + $tenantQuery = ' AND _tenant IN (:_tenant, NULL)'; } $removeQuery .= "( @@ -1467,7 +1467,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql .= $sqlMax . $sqlMin; @@ -1507,7 +1507,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); @@ -1524,7 +1524,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; + $sql .= ' AND _tenant IN (:_tenant, NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); @@ -1661,7 +1661,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if ($this->sharedTables) { - $where[] = "table_main._tenant = :_tenant"; + $where[] = "table_main._tenant IN (:_tenant, NULL)"; } if (Authorization::$status) { @@ -1787,7 +1787,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) } if ($this->sharedTables) { - $where[] = "table_main._tenant = :_tenant"; + $where[] = "table_main._tenant IN (:_tenant, NULL)"; } if (Authorization::$status) { @@ -1850,7 +1850,7 @@ public function sum(string $collection, string $attribute, array $queries = [], } if ($this->sharedTables) { - $where[] = "table_main._tenant = :_tenant"; + $where[] = "table_main._tenant IN (:_tenant, NULL)"; } if (Authorization::$status) { diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 8662cc94f..fdb7d8bea 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -116,7 +116,7 @@ public function getDocument(string $collection, string $id, array $queries = []) "; if ($this->sharedTables) { - $sql .= "AND _tenant = :_tenant"; + $sql .= "AND _tenant IN (:_tenant, NULL)"; } $stmt = $this->getPDO()->prepare($sql); @@ -862,7 +862,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = 'AND _tenant = :_tenant'; + $tenantQuery = 'AND _tenant IN (:_tenant, NULL)'; } return "table_main._uid IN ( diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index ccbac7905..115bd879c 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -601,7 +601,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant"; + $sql .= " AND _tenant IN (:_tenant, NULL)"; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -684,7 +684,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant"; + $sql .= " AND _tenant IN (:_tenant, NULL)"; } $removeQuery = $sql . $removeQuery; @@ -756,7 +756,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant"; + $sql .= " AND _tenant IN (:_tenant, NULL)"; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -879,7 +879,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant"; + $sql .= " AND _tenant IN (:_tenant, NULL)"; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -926,7 +926,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant'; + $tenantQuery = ' AND _tenant IN (:_tenant, NULL)'; } $removeQuery .= "( diff --git a/src/Database/Database.php b/src/Database/Database.php index a3912d411..3f1e40da2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -970,10 +970,6 @@ public function delete(?string $database = null): bool */ public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - $permissions ??= [ Permission::create(Role::any()), ]; @@ -1111,10 +1107,13 @@ public function getCollection(string $id): Document $collection = $this->silent(fn () => $this->getDocument(self::METADATA, $id)); + $tenant = $collection->getAttribute('$tenant'); + if ( $id !== self::METADATA && $this->adapter->getSharedTables() - && $collection->getAttribute('$tenant') != $this->adapter->getTenant() + && $tenant !== null + && $tenant != $this->adapter->getTenant() ) { return new Document(); } @@ -1144,11 +1143,12 @@ public function listCollections(int $limit = 25, int $offset = 0): array Query::offset($offset) ])); - if ($this->adapter->getSharedTables()) { - $result = \array_filter($result, function ($collection) { - return $collection->getAttribute('$tenant') == $this->adapter->getTenant(); - }); - } + // TODO: Should this be required? + //if ($this->adapter->getSharedTables()) { + // $result = \array_filter($result, function ($collection) { + // return $collection->getAttribute('$tenant') == $this->adapter->getTenant(); + // }); + //} $this->trigger(self::EVENT_COLLECTION_LIST, $result); @@ -3087,7 +3087,11 @@ private function populateDocumentRelationships(Document $collection, Document $d */ public function createDocument(string $collection, Document $document): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { + if ( + $this->adapter->getSharedTables() + && empty($this->adapter->getTenant()) + && $collection !== self::METADATA + ) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } From fdc9da7c082936745bbed291b8edeab4cb448e72 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 19:54:03 +1200 Subject: [PATCH 037/256] Don't require tenant for create database --- src/Database/Database.php | 5 +---- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 3f1e40da2..0c2bb2119 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -871,11 +871,8 @@ public function ping(): bool */ public function create(?string $database = null): bool { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } + $database ??= $this->adapter->getDatabase(); - $database = $database ?? $this->adapter->getDatabase(); $this->adapter->create($database); /** diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index c9f495cb3..662a3ac59 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15011,7 +15011,7 @@ public function testEmptyOperatorValues(): void * @throws StructureException * @throws TimeoutException */ - public function testIsolationModes(): void + public function testSharedTables(): void { /** * Default mode already tested, we'll test 'schema' and 'table' isolation here From 8b6e99205a324fbffbe79ee4b79f3a54357f0195 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 20:22:54 +1200 Subject: [PATCH 038/256] Dont require tenant on get metadata collection --- src/Database/Database.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 0c2bb2119..1ffbd377b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1098,7 +1098,11 @@ public function updateCollection(string $id, array $permissions, bool $documentS */ public function getCollection(string $id): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { + if ( + $id !== self::METADATA + && $this->adapter->getSharedTables() + && empty($this->adapter->getTenant()) + ) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } From bcbee88f4a3a57affd19a0eb41ba297098dd2e7d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 20:30:53 +1200 Subject: [PATCH 039/256] Fix check order for getDocument --- src/Database/Database.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 1ffbd377b..fb2020d74 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2652,10 +2652,6 @@ public function deleteIndex(string $collection, string $id): bool */ public function getDocument(string $collection, string $id, array $queries = []): Document { - if ($this->adapter->getSharedTables() && 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(self::COLLECTION); } @@ -2664,6 +2660,10 @@ public function getDocument(string $collection, string $id, array $queries = []) throw new DatabaseException('Collection not found'); } + if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { + throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); + } + if (empty($id)) { return new Document(); } From 0b2cda2cb5a21d3bcbf14ae2fa2dfda6c1aa328c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 20:46:14 +1200 Subject: [PATCH 040/256] Remove exists tenant check --- src/Database/Database.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index fb2020d74..dec567e1e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -901,11 +901,7 @@ public function create(?string $database = null): bool */ public function exists(?string $database = null, ?string $collection = null): bool { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - - $database = $database ?? $this->adapter->getDatabase(); + $database ??= $this->adapter->getDatabase(); return $this->adapter->exists($database, $collection); } From 15dc0ae7d73646d5fb6d57aa830ecdd9c8ef82bd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 21:01:04 +1200 Subject: [PATCH 041/256] Remove redundant missing tenant checks --- src/Database/Database.php | 116 +------------------------------------- 1 file changed, 2 insertions(+), 114 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index dec567e1e..dd17fb514 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -913,10 +913,6 @@ public function exists(?string $database = null, ?string $collection = null): bo */ public function list(): array { - if ($this->adapter->getSharedTables() && 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); @@ -932,10 +928,6 @@ public function list(): array */ public function delete(?string $database = null): bool { - if ($this->adapter->getSharedTables() && 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); @@ -1049,10 +1041,6 @@ public function createCollection(string $id, array $attributes = [], array $inde */ public function updateCollection(string $id, array $permissions, bool $documentSecurity): Document { - if ($this->adapter->getSharedTables() && 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)) { @@ -1131,10 +1119,6 @@ public function getCollection(string $id): Document */ public function listCollections(int $limit = 25, int $offset = 0): array { - if ($this->adapter->getSharedTables() && 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) @@ -1161,10 +1145,6 @@ public function listCollections(int $limit = 25, int $offset = 0): array */ public function getSizeOfCollection(string $collection): int { - if ($this->adapter->getSharedTables() && 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()) { @@ -1188,10 +1168,6 @@ public function getSizeOfCollection(string $collection): int */ public function deleteCollection(string $id): bool { - if ($this->adapter->getSharedTables() && 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()) { @@ -1252,10 +1228,6 @@ 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->getSharedTables() && 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()) { @@ -1438,10 +1410,6 @@ protected function validateDefaultTypes(string $type, mixed $default): void */ protected function updateIndexMeta(string $collection, string $id, callable $updateCallback): Document { - if ($this->adapter->getSharedTables() && 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) { @@ -1481,10 +1449,6 @@ protected function updateIndexMeta(string $collection, string $id, callable $upd */ protected function updateAttributeMeta(string $collection, string $id, callable $updateCallback): Document { - if ($this->adapter->getSharedTables() && 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) { @@ -1741,10 +1705,6 @@ public function updateAttribute(string $collection, string $id, string $type = n */ public function checkAttribute(Document $collection, Document $attribute): bool { - if ($this->adapter->getSharedTables() && 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); @@ -1778,10 +1738,6 @@ public function checkAttribute(Document $collection, Document $attribute): bool */ public function deleteAttribute(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -1852,10 +1808,6 @@ public function deleteAttribute(string $collection, string $id): bool */ public function renameAttribute(string $collection, string $old, string $new): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -1930,10 +1882,6 @@ public function createRelationship( ?string $twoWayKey = null, string $onDelete = Database::RELATION_MUTATE_RESTRICT ): bool { - if ($this->adapter->getSharedTables() && 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()) { @@ -2122,10 +2070,6 @@ public function updateRelationship( ?bool $twoWay = null, ?string $onDelete = null ): bool { - if ($this->adapter->getSharedTables() && 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) @@ -2304,10 +2248,6 @@ public function updateRelationship( */ public function deleteRelationship(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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; @@ -2432,10 +2372,6 @@ public function deleteRelationship(string $collection, string $id): bool */ public function renameIndex(string $collection, string $old, string $new): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -2495,10 +2431,6 @@ 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->getSharedTables() && 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'); } @@ -2606,10 +2538,6 @@ public function createIndex(string $collection, string $id, string $type, array */ public function deleteIndex(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -2656,10 +2584,6 @@ public function getDocument(string $collection, string $id, array $queries = []) throw new DatabaseException('Collection not found'); } - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if (empty($id)) { return new Document(); } @@ -3085,9 +3009,9 @@ private function populateDocumentRelationships(Document $collection, Document $d public function createDocument(string $collection, Document $document): Document { if ( - $this->adapter->getSharedTables() + $collection !== self::METADATA + && $this->adapter->getSharedTables() && empty($this->adapter->getTenant()) - && $collection !== self::METADATA ) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } @@ -3160,10 +3084,6 @@ 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->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if (empty($documents)) { return []; } @@ -3549,10 +3469,6 @@ private function relateDocumentsById( */ public function updateDocument(string $collection, string $id, Document $document): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if (!$id) { throw new DatabaseException('Must define $id attribute'); } @@ -3745,10 +3661,6 @@ 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->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if (empty($documents)) { return []; } @@ -4218,10 +4130,6 @@ 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->getSharedTables() && 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'); } @@ -4308,10 +4216,6 @@ 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->getSharedTables() && 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'); } @@ -4398,10 +4302,6 @@ public function decreaseDocumentAttribute(string $collection, string $id, string */ public function deleteDocument(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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)); @@ -4876,10 +4776,6 @@ public function purgeCachedDocument(string $collectionId, string $id): bool */ public function find(string $collection, array $queries = []): array { - if ($this->adapter->getSharedTables() && 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()) { @@ -5056,10 +4952,6 @@ public function findOne(string $collection, array $queries = []): false|Document */ public function count(string $collection, array $queries = [], ?int $max = null): int { - if ($this->adapter->getSharedTables() && 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', []); @@ -5102,10 +4994,6 @@ public function count(string $collection, array $queries = [], ?int $max = null) */ public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int { - if ($this->adapter->getSharedTables() && 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', []); From 4f91b4abceecd048b90f09128e44927b6c0d6fbc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 21:17:40 +1200 Subject: [PATCH 042/256] Remove get collection check --- src/Database/Database.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index dd17fb514..d5bdf0ff3 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1082,14 +1082,6 @@ public function updateCollection(string $id, array $permissions, bool $documentS */ public function getCollection(string $id): Document { - if ( - $id !== self::METADATA - && $this->adapter->getSharedTables() - && 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)); $tenant = $collection->getAttribute('$tenant'); From 7bc4146a1df044aae127315c2e4ee91bf21720ad Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 21:38:06 +1200 Subject: [PATCH 043/256] Throw on duplicate collection --- src/Database/Adapter/MariaDB.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3506221b7..3b4b6d6c8 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -130,7 +130,7 @@ public function createCollection(string $name, array $attributes = [], array $in } $sql = " - CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id)} ( + CREATE TABLE {$this->getSQLTable($id)} ( _id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, _uid VARCHAR(255) NOT NULL, _createdAt DATETIME(3) DEFAULT NULL, From 9a476e82f7be7f83a1e7060be76f90534c396b33 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 22:34:48 +1200 Subject: [PATCH 044/256] Throw duplicate properly, create collections in transaction --- src/Database/Adapter/MariaDB.php | 68 +++++++++++++++++++------------- src/Database/Adapter/MySQL.php | 11 ++++-- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3b4b6d6c8..afb00379a 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -129,7 +129,7 @@ public function createCollection(string $name, array $attributes = [], array $in $indexStrings[$key] = "{$indexType} `{$indexId}` ({$indexAttributes}),"; } - $sql = " + $collectionStmt = " CREATE TABLE {$this->getSQLTable($id)} ( _id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, _uid VARCHAR(255) NOT NULL, @@ -142,7 +142,7 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { - $sql .= " + $collectionStmt .= " _tenant INT(11) UNSIGNED DEFAULT NULL, UNIQUE KEY _uid (_tenant, _uid), KEY _created_at (_tenant, _createdAt), @@ -150,24 +150,18 @@ public function createCollection(string $name, array $attributes = [], array $in KEY _tenant_id (_tenant, _id) "; } else { - $sql .= " + $collectionStmt .= " UNIQUE KEY _uid (_uid), KEY _created_at (_createdAt), KEY _updated_at (_updatedAt) "; } - $sql .= ")"; - - $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + $collectionStmt .= ")"; + $collectionStmt = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collectionStmt); - try { - $this->getPDO() - ->prepare($sql) - ->execute(); - - $sql = " - CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id . '_perms')} ( + $permissionsStmt = " + CREATE TABLE {$this->getSQLTable($id . '_perms')} ( _id int(11) UNSIGNED NOT NULL AUTO_INCREMENT, _type VARCHAR(12) NOT NULL, _permission VARCHAR(255) NOT NULL, @@ -175,31 +169,46 @@ public function createCollection(string $name, array $attributes = [], array $in PRIMARY KEY (_id), "; - if ($this->sharedTables) { - $sql .= " + if ($this->sharedTables) { + $permissionsStmt .= " _tenant INT(11) UNSIGNED DEFAULT NULL, UNIQUE INDEX _index1 (_document, _tenant, _type, _permission), INDEX _permission (_tenant, _permission, _type) "; - } else { - $sql .= " + } else { + $permissionsStmt .= " UNIQUE INDEX _index1 (_document, _type, _permission), INDEX _permission (_permission, _type) "; - } + } - $sql .= ")"; + $permissionsStmt .= ")"; + $permissionsStmt = $this->trigger(Database::EVENT_COLLECTION_CREATE, $permissionsStmt); - $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + try { + $this->pdo->beginTransaction(); $this->getPDO() - ->prepare($sql) + ->prepare($collectionStmt) ->execute(); - } catch (\Exception $th) { + $this->getPDO() - ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->prepare($permissionsStmt) ->execute(); - throw $th; + + if (!$this->pdo->commit()) { + throw new DatabaseException('Failed to commit transaction'); + } + } catch (\Exception $e) { + if (!$this->pdo->inTransaction()) { + $this->pdo->rollBack(); + } + + if ($e instanceof PDOException) { + $this->processException($e); + } + + throw $e; } return true; @@ -2246,17 +2255,20 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL /** * @param PDOException $e * @throws TimeoutException + * @throws DuplicateException */ protected function processException(PDOException $e): void { - // Regular PDO if ($e->getCode() === '70100' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1969) { throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + } else if ($e->getCode() === 1969 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '70100') { + throw new TimeoutException($e->getMessage(), $e->getCode(), $e); } - // PDOProxy switches errorInfo PDOProxy.php line 64 - if ($e->getCode() === 1969 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '70100') { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } else if ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); } throw $e; diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 287680390..40ee49d01 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -5,6 +5,7 @@ use PDOException; use Utopia\Database\Database; use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; class MySQL extends MariaDB @@ -37,16 +38,20 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL /** * @param PDOException $e * @throws TimeoutException + * @throws DuplicateException */ protected function processException(PDOException $e): void { if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { + throw new TimeoutException($e->getMessage(), $e->getCode(), $e); } - // PDOProxy which who switches errorInfo - if ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } else if ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); } throw $e; From 4dc980c240dfcf503b2a6208ef689dc69e27f79a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 22:41:53 +1200 Subject: [PATCH 045/256] Use getPDO --- src/Database/Adapter/MariaDB.php | 36 ++++++++++++++++---------------- src/Database/Adapter/MySQL.php | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index afb00379a..ccece4414 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -129,7 +129,7 @@ public function createCollection(string $name, array $attributes = [], array $in $indexStrings[$key] = "{$indexType} `{$indexId}` ({$indexAttributes}),"; } - $collectionStmt = " + $collection = " CREATE TABLE {$this->getSQLTable($id)} ( _id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, _uid VARCHAR(255) NOT NULL, @@ -142,7 +142,7 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { - $collectionStmt .= " + $collection .= " _tenant INT(11) UNSIGNED DEFAULT NULL, UNIQUE KEY _uid (_tenant, _uid), KEY _created_at (_tenant, _createdAt), @@ -150,17 +150,17 @@ public function createCollection(string $name, array $attributes = [], array $in KEY _tenant_id (_tenant, _id) "; } else { - $collectionStmt .= " + $collection .= " UNIQUE KEY _uid (_uid), KEY _created_at (_createdAt), KEY _updated_at (_updatedAt) "; } - $collectionStmt .= ")"; - $collectionStmt = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collectionStmt); + $collection .= ")"; + $collection = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collection); - $permissionsStmt = " + $permissions = " CREATE TABLE {$this->getSQLTable($id . '_perms')} ( _id int(11) UNSIGNED NOT NULL AUTO_INCREMENT, _type VARCHAR(12) NOT NULL, @@ -170,38 +170,38 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { - $permissionsStmt .= " + $permissions .= " _tenant INT(11) UNSIGNED DEFAULT NULL, UNIQUE INDEX _index1 (_document, _tenant, _type, _permission), INDEX _permission (_tenant, _permission, _type) "; } else { - $permissionsStmt .= " + $permissions .= " UNIQUE INDEX _index1 (_document, _type, _permission), INDEX _permission (_permission, _type) "; } - $permissionsStmt .= ")"; - $permissionsStmt = $this->trigger(Database::EVENT_COLLECTION_CREATE, $permissionsStmt); + $permissions .= ")"; + $permissions = $this->trigger(Database::EVENT_COLLECTION_CREATE, $permissions); try { - $this->pdo->beginTransaction(); + $this->getPDO()->beginTransaction(); $this->getPDO() - ->prepare($collectionStmt) + ->prepare($collection) ->execute(); $this->getPDO() - ->prepare($permissionsStmt) + ->prepare($permissions) ->execute(); - if (!$this->pdo->commit()) { + if (!$this->getPDO()->commit()) { throw new DatabaseException('Failed to commit transaction'); } } catch (\Exception $e) { - if (!$this->pdo->inTransaction()) { - $this->pdo->rollBack(); + if (!$this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); } if ($e instanceof PDOException) { @@ -2261,13 +2261,13 @@ protected function processException(PDOException $e): void { if ($e->getCode() === '70100' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1969) { throw new TimeoutException($e->getMessage(), $e->getCode(), $e); - } else if ($e->getCode() === 1969 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '70100') { + } elseif ($e->getCode() === 1969 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '70100') { throw new TimeoutException($e->getMessage(), $e->getCode(), $e); } if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { throw new DuplicateException($e->getMessage(), $e->getCode(), $e); - } else if ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { + } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { throw new DuplicateException($e->getMessage(), $e->getCode(), $e); } diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 40ee49d01..51c1c627d 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -50,7 +50,7 @@ protected function processException(PDOException $e): void if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { throw new DuplicateException($e->getMessage(), $e->getCode(), $e); - } else if ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { + } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { throw new DuplicateException($e->getMessage(), $e->getCode(), $e); } From 88570af189320e8bd6da588203a4c4f3b2999e88 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 22:46:49 +1200 Subject: [PATCH 046/256] Fix check --- src/Database/Adapter/MariaDB.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ccece4414..f32a8c167 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -200,7 +200,7 @@ public function createCollection(string $name, array $attributes = [], array $in throw new DatabaseException('Failed to commit transaction'); } } catch (\Exception $e) { - if (!$this->getPDO()->inTransaction()) { + if ($this->getPDO()->inTransaction()) { $this->getPDO()->rollBack(); } From df9ef171c591bdb7491f727f04f3f3806e8e09a8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 23:25:08 +1200 Subject: [PATCH 047/256] Fix impicit commit --- composer.json | 2 +- composer.lock | 36 +++++++++++----------------- src/Database/Adapter/MariaDB.php | 40 ++++++++++++++------------------ 3 files changed, 31 insertions(+), 47 deletions(-) diff --git a/composer.json b/composer.json index cab94655c..52f8a44f8 100755 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "fakerphp/faker": "^1.14", "phpunit/phpunit": "^9.4", "pcov/clobber": "^2.0", - "swoole/ide-helper": "4.8.0", + "swoole/ide-helper": "5.1.2", "utopia-php/cli": "^0.14.0", "laravel/pint": "1.13.*", "phpstan/phpstan": "1.10.*", diff --git a/composer.lock b/composer.lock index 41ee9ffbf..aca8e7724 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b71e4329463ee71c5067fa62792a52f", + "content-hash": "1ef7c11d9a0628b3c57c8c80dbebbba1", "packages": [ { "name": "jean85/pretty-package-versions", @@ -266,16 +266,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.6", + "version": "0.33.7", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6" + "reference": "78d293d99a262bd63ece750bbf989c7e0643b825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", - "reference": "8fe57da0cecd57e3b17cd395b4a666a24f4c07a6", + "url": "https://api.github.com/repos/utopia-php/http/zipball/78d293d99a262bd63ece750bbf989c7e0643b825", + "reference": "78d293d99a262bd63ece750bbf989c7e0643b825", "shasum": "" }, "require": { @@ -305,9 +305,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.6" + "source": "https://github.com/utopia-php/http/tree/0.33.7" }, - "time": "2024-03-21T18:10:57+00:00" + "time": "2024-08-01T14:01:04+00:00" }, { "name": "utopia-php/mongo", @@ -2382,16 +2382,16 @@ }, { "name": "swoole/ide-helper", - "version": "4.8.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/swoole/ide-helper.git", - "reference": "837a2b20242e3cebf0ba1168e876f0f1ca9a14e3" + "reference": "33ec7af9111b76d06a70dd31191cc74793551112" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swoole/ide-helper/zipball/837a2b20242e3cebf0ba1168e876f0f1ca9a14e3", - "reference": "837a2b20242e3cebf0ba1168e876f0f1ca9a14e3", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/33ec7af9111b76d06a70dd31191cc74793551112", + "reference": "33ec7af9111b76d06a70dd31191cc74793551112", "shasum": "" }, "type": "library", @@ -2408,19 +2408,9 @@ "description": "IDE help files for Swoole.", "support": { "issues": "https://github.com/swoole/ide-helper/issues", - "source": "https://github.com/swoole/ide-helper/tree/4.8.0" + "source": "https://github.com/swoole/ide-helper/tree/5.1.2" }, - "funding": [ - { - "url": "https://gitee.com/swoole/swoole?donate=true", - "type": "custom" - }, - { - "url": "https://github.com/swoole", - "type": "github" - } - ], - "time": "2021-10-14T19:39:28+00:00" + "time": "2024-02-01T22:28:11+00:00" }, { "name": "symfony/deprecation-contracts", diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index f32a8c167..478bbb7e3 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -161,33 +161,31 @@ public function createCollection(string $name, array $attributes = [], array $in $collection = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collection); $permissions = " - CREATE TABLE {$this->getSQLTable($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), - "; + CREATE TABLE {$this->getSQLTable($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), + "; if ($this->sharedTables) { $permissions .= " - _tenant INT(11) UNSIGNED DEFAULT NULL, - UNIQUE INDEX _index1 (_document, _tenant, _type, _permission), - INDEX _permission (_tenant, _permission, _type) - "; + _tenant INT(11) UNSIGNED DEFAULT NULL, + UNIQUE INDEX _index1 (_document, _tenant, _type, _permission), + INDEX _permission (_tenant, _permission, _type) + "; } else { $permissions .= " - UNIQUE INDEX _index1 (_document, _type, _permission), - INDEX _permission (_permission, _type) - "; + UNIQUE INDEX _index1 (_document, _type, _permission), + INDEX _permission (_permission, _type) + "; } $permissions .= ")"; $permissions = $this->trigger(Database::EVENT_COLLECTION_CREATE, $permissions); try { - $this->getPDO()->beginTransaction(); - $this->getPDO() ->prepare($collection) ->execute(); @@ -195,14 +193,10 @@ public function createCollection(string $name, array $attributes = [], array $in $this->getPDO() ->prepare($permissions) ->execute(); - - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } } catch (\Exception $e) { - if ($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } + $this->getPDO() + ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->execute(); if ($e instanceof PDOException) { $this->processException($e); From 27444b13495111e6179b14fa53ca55ddf91d192e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 12 Aug 2024 23:43:30 +1200 Subject: [PATCH 048/256] Update null checks --- src/Database/Adapter/MariaDB.php | 16 ++++++++-------- src/Database/Adapter/Postgres.php | 16 ++++++++-------- src/Database/Adapter/SQL.php | 4 ++-- src/Database/Adapter/SQLite.php | 10 +++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 478bbb7e3..6bc8f3238 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1049,7 +1049,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1126,7 +1126,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $removeQuery = $sql . $removeQuery; @@ -1213,7 +1213,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -1342,7 +1342,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1385,7 +1385,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant IN (:_tenant, NULL)'; + $tenantQuery = ' AND _tenant = :_tenant OR _tenant IS NULL'; } $removeQuery .= "( @@ -1566,7 +1566,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql .= $sqlMax . $sqlMin; @@ -1608,7 +1608,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); @@ -1627,7 +1627,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index b6e98b069..859a339b0 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -977,7 +977,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1057,7 +1057,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $removeQuery = $sql . $removeQuery; @@ -1129,7 +1129,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -1253,7 +1253,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1296,7 +1296,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant IN (:_tenant, NULL)'; + $tenantQuery = ' AND _tenant = :_tenant OR _tenant IS NULL'; } $removeQuery .= "( @@ -1467,7 +1467,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql .= $sqlMax . $sqlMin; @@ -1507,7 +1507,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); @@ -1524,7 +1524,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant IN (:_tenant, NULL)'; + $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index fdb7d8bea..e50f0ae03 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -116,7 +116,7 @@ public function getDocument(string $collection, string $id, array $queries = []) "; if ($this->sharedTables) { - $sql .= "AND _tenant IN (:_tenant, NULL)"; + $sql .= "AND _tenant = :_tenant OR _tenant IS NULL"; } $stmt = $this->getPDO()->prepare($sql); @@ -862,7 +862,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = 'AND _tenant IN (:_tenant, NULL)'; + $tenantQuery = 'AND _tenant = :_tenant OR _tenant IS NULL'; } return "table_main._uid IN ( diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 115bd879c..16e1bf7cd 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -601,7 +601,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant IN (:_tenant, NULL)"; + $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -684,7 +684,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant IN (:_tenant, NULL)"; + $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; } $removeQuery = $sql . $removeQuery; @@ -756,7 +756,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant IN (:_tenant, NULL)"; + $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -879,7 +879,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= " AND _tenant IN (:_tenant, NULL)"; + $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -926,7 +926,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant IN (:_tenant, NULL)'; + $tenantQuery = ' AND _tenant = :_tenant OR _tenant IS NULL'; } $removeQuery .= "( From ac68fa45dd2fa513241f24723606667b1284d2a4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 13 Aug 2024 16:04:36 +1200 Subject: [PATCH 049/256] Don't drop collection on duplicate exception --- src/Database/Adapter/MariaDB.php | 34 +++++++-------- src/Database/Adapter/Mongo.php | 10 ++--- src/Database/Adapter/MySQL.php | 41 ++++++++---------- src/Database/Adapter/Postgres.php | 72 ++++++++++++++++--------------- tests/e2e/Adapter/Base.php | 21 +++++++++ 5 files changed, 97 insertions(+), 81 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 6bc8f3238..0e5e155c6 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -193,13 +193,13 @@ public function createCollection(string $name, array $attributes = [], array $in $this->getPDO() ->prepare($permissions) ->execute(); - } catch (\Exception $e) { - $this->getPDO() - ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") - ->execute(); + } catch (PDOException $e) { + $e = $this->processException($e); - if ($e instanceof PDOException) { - $this->processException($e); + if (!($e instanceof DuplicateException)) { + $this->getPDO() + ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->execute(); } throw $e; @@ -1677,6 +1677,7 @@ public function deleteDocument(string $collection, string $id): bool * @return array * @throws DatabaseException * @throws TimeoutException + * @throws Exception */ public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array { @@ -1827,7 +1828,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, try { $stmt->execute(); } catch (PDOException $e) { - $this->processException($e); + throw $this->processException($e); } $results = $stmt->fetchAll(); @@ -2246,25 +2247,22 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL }); } - /** - * @param PDOException $e - * @throws TimeoutException - * @throws DuplicateException - */ - protected function processException(PDOException $e): void + protected function processException(PDOException $e): \Exception { + // Timeout if ($e->getCode() === '70100' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1969) { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + return new TimeoutException($e->getMessage(), $e->getCode(), $e); } elseif ($e->getCode() === 1969 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '70100') { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); + return new TimeoutException($e->getMessage(), $e->getCode(), $e); } + // Duplicate table if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { - throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException($e->getMessage(), $e->getCode(), $e); } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { - throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException($e->getMessage(), $e->getCode(), $e); } - throw $e; + return $e; } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index de4da64f9..96e173ab1 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1755,17 +1755,13 @@ public function getKeywords(): array return []; } - /** - * @throws Timeout - * @throws Exception - */ - protected function processException(Exception $e): void + protected function processException(Exception $e): \Exception { if ($e->getCode() === 50) { - throw new Timeout($e->getMessage()); + return new Timeout($e->getMessage()); } - throw $e; + return $e; } /** diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 51c1c627d..ab821db8a 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -35,28 +35,6 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL }); } - /** - * @param PDOException $e - * @throws TimeoutException - * @throws DuplicateException - */ - protected function processException(PDOException $e): void - { - if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { - throw new TimeoutException($e->getMessage(), $e->getCode(), $e); - } - - if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { - throw new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { - throw new DuplicateException($e->getMessage(), $e->getCode(), $e); - } - - throw $e; - } - /** * Get Collection Size * @param string $collection @@ -104,4 +82,23 @@ public function castIndexArray(): bool { return true; } + + protected function processException(PDOException $e): \Exception + { + // Timeout + if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { + return new TimeoutException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { + return new TimeoutException($e->getMessage(), $e->getCode(), $e); + } + + // Duplicate table + if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { + return new DuplicateException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { + return new DuplicateException($e->getMessage(), $e->getCode(), $e); + } + + return $e; + } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 859a339b0..bff8383b1 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -9,8 +9,8 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; -use Utopia\Database\Exception\Duplicate; -use Utopia\Database\Exception\Timeout; +use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; @@ -827,7 +827,7 @@ public function createDocument(string $collection, Document $document): Document switch ($e->getCode()) { case 23505: $this->getPDO()->rollBack(); - throw new Duplicate('Duplicated document: ' . $e->getMessage()); + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); default: throw $e; } @@ -849,7 +849,7 @@ public function createDocument(string $collection, Document $document): Document * * @return array * - * @throws Duplicate + * @throws DuplicateException */ public function createDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { @@ -942,7 +942,7 @@ public function createDocuments(string $collection, array $documents, int $batch $this->getPDO()->rollBack(); throw match ($e->getCode()) { - 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), default => $e, }; } @@ -955,6 +955,8 @@ public function createDocuments(string $collection, array $documents, int $batch * @param Document $document * * @return Document + * @throws DatabaseException + * @throws DuplicateException */ public function updateDocument(string $collection, Document $document): Document { @@ -1164,11 +1166,12 @@ public function updateDocument(string $collection, Document $document): Document } } catch (PDOException $e) { $this->getPDO()->rollBack(); + + // Must be a switch for loose match switch ($e->getCode()) { case 1062: case 23505: - throw new Duplicate('Duplicated document: ' . $e->getMessage()); - + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); default: throw $e; } @@ -1187,10 +1190,9 @@ public function updateDocument(string $collection, Document $document): Document * @param string $collection * @param array $documents * @param int $batchSize - * * @return array - * - * @throws Duplicate + * @throws DatabaseException + * @throws DuplicateException */ public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { @@ -1430,10 +1432,14 @@ public function updateDocuments(string $collection, array $documents, int $batch } catch (PDOException $e) { $this->getPDO()->rollBack(); - throw match ($e->getCode()) { - 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), - default => $e, - }; + // Must be a switch for loose match + switch ($e->getCode()) { + case 1062: + case 23505: + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); + default: + throw $e; + } } } @@ -1575,9 +1581,8 @@ public function deleteDocument(string $collection, string $id): bool * @param string $cursorDirection * * @return array - * @throws Exception - * @throws PDOException - * @throws Timeout + * @throws DatabaseException + * @throws TimeoutException */ public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array { @@ -2195,29 +2200,28 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL } /** - * @param PDOException $e - * @throws Timeout + * @return string */ - protected function processException(PDOException $e): void + public function getLikeOperator(): string + { + return 'ILIKE'; + } + + protected function processException(PDOException $e): \Exception { - // Regular PDO + // Timeout if ($e->getCode() === '57014' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - throw new Timeout($e->getMessage(), $e->getCode(), $e); + return new TimeoutException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 7 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '57014') { + return new TimeoutException($e->getMessage(), $e->getCode(), $e); } - // PDOProxy switches errorInfo PDOProxy.php line 64 - if ($e->getCode() === 7 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '57014') { - throw new Timeout($e->getMessage(), $e->getCode(), $e); + if ($e->getCode() === '42P07' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { + return new DuplicateException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 7 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42P07') { + return new DuplicateException($e->getMessage(), $e->getCode(), $e); } - throw $e; - } - - /** - * @return string - */ - public function getLikeOperator(): string - { - return 'ILIKE'; + return $e; } } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 662a3ac59..41f90f3de 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -79,6 +79,27 @@ public function testCreateExistsDelete(): void $this->assertEquals(true, static::getDatabase()->create()); } + /** + * @throws LimitException + * @throws DuplicateException + * @throws DatabaseException + */ + public function testCreateDuplicates(): void + { + static::getDatabase()->createCollection('test', permissions: [ + Permission::read(Role::any()) + ]); + + try { + static::getDatabase()->createCollection('test'); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(DuplicateException::class, $e); + } + + $this->assertNotEmpty(static::getDatabase()->listCollections()); + } + public function testUpdateDeleteCollectionNotFound(): void { try { From d7ae07e0b431c8245a72fb10aea6c08a0290dc4d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 13 Aug 2024 20:57:53 +1200 Subject: [PATCH 050/256] Fix precedence --- src/Database/Adapter/MariaDB.php | 16 ++++++++-------- src/Database/Adapter/Postgres.php | 16 ++++++++-------- src/Database/Adapter/SQL.php | 4 ++-- src/Database/Adapter/SQLite.php | 10 +++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0e5e155c6..9738d0ecf 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1049,7 +1049,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1126,7 +1126,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $removeQuery = $sql . $removeQuery; @@ -1213,7 +1213,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -1342,7 +1342,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1385,7 +1385,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant OR _tenant IS NULL'; + $tenantQuery = ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $removeQuery .= "( @@ -1566,7 +1566,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql .= $sqlMax . $sqlMin; @@ -1608,7 +1608,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); @@ -1627,7 +1627,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index bff8383b1..7196fbdfc 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -979,7 +979,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1059,7 +1059,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $removeQuery = $sql . $removeQuery; @@ -1131,7 +1131,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -1255,7 +1255,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -1298,7 +1298,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant OR _tenant IS NULL'; + $tenantQuery = ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $removeQuery .= "( @@ -1473,7 +1473,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql .= $sqlMax . $sqlMin; @@ -1513,7 +1513,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_DOCUMENT_DELETE, $sql); @@ -1530,7 +1530,7 @@ public function deleteDocument(string $collection, string $id): bool "; if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant OR _tenant IS NULL'; + $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index e50f0ae03..2b76380f8 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -116,7 +116,7 @@ public function getDocument(string $collection, string $id, array $queries = []) "; if ($this->sharedTables) { - $sql .= "AND _tenant = :_tenant OR _tenant IS NULL"; + $sql .= "AND (_tenant = :_tenant OR _tenant IS NULL)"; } $stmt = $this->getPDO()->prepare($sql); @@ -862,7 +862,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = 'AND _tenant = :_tenant OR _tenant IS NULL'; + $tenantQuery = 'AND (_tenant = :_tenant OR _tenant IS NULL)'; } return "table_main._uid IN ( diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 16e1bf7cd..acadb025f 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -601,7 +601,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; + $sql .= " AND (_tenant = :_tenant OR _tenant IS NULL)"; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -684,7 +684,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; + $sql .= " AND (_tenant = :_tenant OR _tenant IS NULL)"; } $removeQuery = $sql . $removeQuery; @@ -756,7 +756,7 @@ public function updateDocument(string $collection, Document $document): Document "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; + $sql .= " AND (_tenant = :_tenant OR _tenant IS NULL)"; } $sql = $this->trigger(Database::EVENT_DOCUMENT_UPDATE, $sql); @@ -879,7 +879,7 @@ public function updateDocuments(string $collection, array $documents, int $batch "; if ($this->sharedTables) { - $sql .= " AND _tenant = :_tenant OR _tenant IS NULL"; + $sql .= " AND (_tenant = :_tenant OR _tenant IS NULL)"; } $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); @@ -926,7 +926,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $tenantQuery = ''; if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant OR _tenant IS NULL'; + $tenantQuery = ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } $removeQuery .= "( From 04ffb860d85054b2d2bc18e9ad2a7d19d8719c13 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 13 Aug 2024 23:01:34 +1200 Subject: [PATCH 051/256] Allow getTenant --- src/Database/Database.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 4cafdc24d..9dcee89ac 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -825,6 +825,18 @@ public function setTenant(?int $tenant): static return $this; } + /** + * Get Tenant + * + * Get tenant to use if tables are shared + * + * @return ?int + */ + public function getTenant(): ?int + { + return $this->adapter->getTenant(); + } + public function setPreserveDates(bool $preserve): static { $this->preserveDates = $preserve; From e37b23fb2eeba1e49a12ddca8438dacf23105219 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 Aug 2024 13:05:13 +1200 Subject: [PATCH 052/256] Re-order properties --- src/Database/Database.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 08f872065..321d3f5e5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -136,17 +136,6 @@ class Database public const INSERT_BATCH_SIZE = 100; - protected Adapter $adapter; - - protected Cache $cache; - - protected string $cacheName = 'default'; - - /** - * @var array - */ - protected array $map = []; - /** * List of Internal attributes * @@ -288,6 +277,17 @@ class Database 'indexes' => [], ]; + protected Adapter $adapter; + + protected Cache $cache; + + protected string $cacheName = 'default'; + + /** + * @var array + */ + protected array $map = []; + /** * @var array */ From 545192b81a1ee1d6675c7d197537bf0d0891c3d3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Aug 2024 20:47:43 +1200 Subject: [PATCH 053/256] Add withTenant, withPreservedDates --- src/Database/Database.php | 130 ++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 321d3f5e5..556c7837a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -299,14 +299,14 @@ class Database protected array $instanceFilters = []; /** - * @var array + * @var array> */ protected array $listeners = [ '*' => [], ]; /** - * Array in which the keys are the names of databse listeners that + * Array in which the keys are the names of database listeners that * should be skipped when dispatching events. null $silentListeners * will skip all listeners. * @@ -347,8 +347,11 @@ class Database * @param Cache $cache * @param array $filters */ - public function __construct(Adapter $adapter, Cache $cache, array $filters = []) - { + public function __construct( + Adapter $adapter, + Cache $cache, + array $filters = [] + ) { $this->adapter = $adapter; $this->cache = $cache; $this->instanceFilters = $filters; @@ -724,7 +727,6 @@ public function clearTimeout(string $event = Database::EVENT_ALL): void public function enableFilters(): static { $this->filter = true; - return $this; } @@ -736,10 +738,30 @@ public function enableFilters(): static public function disableFilters(): static { $this->filter = false; - return $this; } + /** + * Skip filters + * + * Execute a callback without filters + * + * @template T + * @param callable(): T $callback + * @return T + */ + public function skipFilters(callable $callback): mixed + { + $initial = $this->filter; + $this->disableFilters(); + + try { + return $callback(); + } finally { + $this->filter = $initial; + } + } + /** * Get instance filters * @@ -777,7 +799,7 @@ public function disableValidation(): static /** * Skip Validation * - * Skips validation for the code to be executed inside the callback + * Execute a callback without validation * * @template T * @param callable(): T $callback @@ -796,7 +818,18 @@ public function skipValidation(callable $callback): mixed } /** - * Set Share Tables + * Get shared tables + * + * Get whether to share tables between tenants + * @return bool + */ + public function getSharedTables(): bool + { + return $this->adapter->getSharedTables(); + } + + /** + * Set shard tables * * Set whether to share tables between tenants * @@ -837,6 +870,32 @@ public function getTenant(): ?int return $this->adapter->getTenant(); } + /** + * With Tenant + * + * Execute a callback with a specific tenant + * + * @param int|null $tenant + * @param callable $callback + * @return mixed + */ + public function withTenant(?int $tenant, callable $callback): mixed + { + $previous = $this->adapter->getTenant(); + $this->adapter->setTenant($tenant); + + try { + return $callback(); + } finally { + $this->adapter->setTenant($previous); + } + } + + public function getPreserveDates(): bool + { + return $this->preserveDates; + } + public function setPreserveDates(bool $preserve): static { $this->preserveDates = $preserve; @@ -844,6 +903,18 @@ public function setPreserveDates(bool $preserve): static return $this; } + public function withPreservedDates(bool $preserve, callable $callback): mixed + { + $previous = $this->preserveDates; + $this->preserveDates = $preserve; + + try { + return $callback(); + } finally { + $this->preserveDates = $previous; + } + } + /** * Get list of keywords that cannot be used * @@ -877,9 +948,11 @@ public function ping(): bool /** * Create the database * - * @throws DatabaseException - * + * @param string|null $database * @return bool + * @throws DuplicateException + * @throws LimitException + * @throws Exception */ public function create(?string $database = null): bool { @@ -1129,13 +1202,6 @@ public function listCollections(int $limit = 25, int $offset = 0): array Query::offset($offset) ])); - // TODO: Should this be required? - //if ($this->adapter->getSharedTables()) { - // $result = \array_filter($result, function ($collection) { - // return $collection->getAttribute('$tenant') == $this->adapter->getTenant(); - // }); - //} - $this->trigger(self::EVENT_COLLECTION_LIST, $result); return $result; @@ -1147,6 +1213,7 @@ public function listCollections(int $limit = 25, int $offset = 0): array * @param string $collection * * @return int + * @throws Exception */ public function getSizeOfCollection(string $collection): int { @@ -1230,6 +1297,7 @@ public function deleteCollection(string $id): bool * @throws DuplicateException * @throws LimitException * @throws StructureException + * @throws Exception */ 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 { @@ -1239,11 +1307,7 @@ public function createAttribute(string $collection, string $id, string $type, in throw new DatabaseException('Collection not found'); } - if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); - } - - // Attribute IDs are case insensitive + // Attribute IDs are case-insensitive $attributes = $collection->getAttribute('attributes', []); /** @var array $attributes */ foreach ($attributes as $attribute) { @@ -1252,9 +1316,9 @@ public function createAttribute(string $collection, string $id, string $type, in } } - /** Ensure required filters for the attribute are passed */ + // Ensure required filters for the attribute are passed $requiredFilters = $this->getRequiredFilters($type); - if (!empty(array_diff($requiredFilters, $filters))) { + if (!empty(\array_diff($requiredFilters, $filters))) { throw new DatabaseException("Attribute of type: $type requires the following filters: " . implode(",", $requiredFilters)); } @@ -1316,7 +1380,7 @@ public function createAttribute(string $collection, string $id, string $type, in throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP); } - // only execute when $default is given + // Only execute when $default is given if (!\is_null($default)) { if ($required === true) { throw new DatabaseException('Cannot set a default value on a required attribute'); @@ -1364,7 +1428,7 @@ protected function getRequiredFilters(?string $type): array * @param string $type Type of the attribute * @param mixed $default Default value of the attribute * - * @throws Exception + * @throws DatabaseException * @return void */ protected function validateDefaultTypes(string $type, mixed $default): void @@ -2724,7 +2788,6 @@ public function getDocument(string $collection, string $id, array $queries = []) /** * Bug with function purity in PHPStan means it thinks $this->map is always empty - * * @phpstan-ignore-next-line */ foreach ($this->map as $key => $value) { @@ -2740,10 +2803,10 @@ public function getDocument(string $collection, string $id, array $queries = []) } } - // Don't save to cache if it's part of a two-way relationship or a relationship at all + // Don't save to cache if it's part of a relationship if (!$hasTwoWayRelationship && empty($relationships)) { $this->cache->save($documentCacheKey, $document->getArrayCopy(), $documentCacheHash); - //add document reference to the collection key + // Add document reference to the collection key $this->cache->save($collectionCacheKey, 'empty', $documentCacheKey); } @@ -2754,7 +2817,7 @@ public function getDocument(string $collection, string $id, array $queries = []) if ($query->getMethod() === Query::TYPE_SELECT) { $values = $query->getValues(); foreach ($this->getInternalAttributes() as $internalAttribute) { - if (!in_array($internalAttribute['$id'], $values)) { + if (!\in_array($internalAttribute['$id'], $values)) { $document->removeAttribute($internalAttribute['$id']); } } @@ -4782,6 +4845,7 @@ public function purgeCachedDocument(string $collectionId, string $id): bool * @throws DatabaseException * @throws QueryException * @throws TimeoutException + * @throws Exception */ public function find(string $collection, array $queries = []): array { @@ -5067,12 +5131,12 @@ public function encode(Document $collection, Document $document): Document $filters = $attribute['filters'] ?? []; $value = $document->getAttribute($key); - // continue on optional param with no default + // Continue on optional param with no default if (is_null($value) && is_null($default)) { continue; } - // assign default only if no value provided + // Assign default only if no value provided // False positive "Call to function is_null() with mixed will always evaluate to false" // @phpstan-ignore-next-line if (is_null($value) && !is_null($default)) { @@ -5256,7 +5320,7 @@ protected function encodeAttribute(string $name, mixed $value, Document $documen } try { - if (array_key_exists($name, $this->instanceFilters)) { + if (\array_key_exists($name, $this->instanceFilters)) { $value = $this->instanceFilters[$name]['encode']($value, $document, $this); } else { $value = self::$filters[$name]['encode']($value, $document, $this); From 0682d0b1e14910008cceeb6d38dfd8187d31aed7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Aug 2024 21:30:07 +1200 Subject: [PATCH 054/256] Always preverse in withPreserveDates --- src/Database/Database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 556c7837a..0eb012abf 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -903,10 +903,10 @@ public function setPreserveDates(bool $preserve): static return $this; } - public function withPreservedDates(bool $preserve, callable $callback): mixed + public function withPreserveDates(callable $callback): mixed { $previous = $this->preserveDates; - $this->preserveDates = $preserve; + $this->preserveDates = true; try { return $callback(); From 6662a2cc5205dac0f71916679be615935972f4dc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 16 Aug 2024 20:18:30 +1200 Subject: [PATCH 055/256] Fix tests --- tests/e2e/Adapter/Base.php | 53 ++++---------------------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index bc5a6ace2..7223103d5 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -85,18 +85,20 @@ public function testCreateExistsDelete(): void */ public function testCreateDuplicates(): void { - static::getDatabase()->createCollection('test', permissions: [ + static::getDatabase()->createCollection('duplicates', permissions: [ Permission::read(Role::any()) ]); try { - static::getDatabase()->createCollection('test'); + static::getDatabase()->createCollection('duplicates'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertInstanceOf(DuplicateException::class, $e); } $this->assertNotEmpty(static::getDatabase()->listCollections()); + + static::getDatabase()->deleteCollection('duplicates'); } public function testUpdateDeleteCollectionNotFound(): void @@ -15206,7 +15208,7 @@ public function testSharedTables(): void $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()); + $this->assertEquals('Collection not found', $e->getMessage()); } // Reset state @@ -15215,51 +15217,6 @@ public function testSharedTables(): void $database->setDatabase($this->testDatabase); } - public function testSharedTablesDuplicateAttributesDontThrow(): void - { - $database = static::getDatabase(); - - if (!$database->getAdapter()->getSupportForAttributes()) { - $this->expectNotToPerformAssertions(); - return; - } - - if ($database->exists('sharedTables')) { - $database->setDatabase('sharedTables')->delete(); - } - - $database - ->setDatabase('sharedTables') - ->setNamespace('') - ->setSharedTables(true) - ->setTenant(1) - ->create(); - - // Create collection - $database->createCollection('duplicates', documentSecurity: false); - $database->createAttribute('duplicates', 'name', Database::VAR_STRING, 10, false); - - $database->setTenant(2); - try { - $database->createCollection('duplicates', documentSecurity: false); - } catch (DuplicateException) { - // Ignore - } - $database->createAttribute('duplicates', 'name', Database::VAR_STRING, 10, false); - - $collection = $database->getCollection('duplicates'); - $this->assertEquals(1, \count($collection->getAttribute('attributes'))); - - $database->setTenant(1); - - $collection = $database->getCollection('duplicates'); - $this->assertEquals(1, \count($collection->getAttribute('attributes'))); - - $database->setSharedTables(false); - $database->setNamespace(static::$namespace); - $database->setDatabase($this->testDatabase); - } - public function testTransformations(): void { static::getDatabase()->createCollection('docs', attributes: [ From da8113e0514fdaeedd6b13e36b3f02d547f7dfe0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 15:22:39 +1200 Subject: [PATCH 056/256] Fix in cases --- src/Database/Adapter/MariaDB.php | 8 +- src/Database/Adapter/Postgres.php | 137 +++++++++++++++++++----------- tests/e2e/Adapter/Base.php | 8 +- 3 files changed, 93 insertions(+), 60 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index fcaf38c6c..7aed62094 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -923,10 +923,8 @@ public function createDocuments(string $collection, array $documents, int $batch try { $this->getPDO()->beginTransaction(); - $name = $this->filter($collection); $batches = \array_chunk($documents, \max(1, $batchSize)); - $internalIds = []; foreach ($batches as $batch) { @@ -1812,7 +1810,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if ($this->sharedTables) { - $where[] = "table_main._tenant IN (:_tenant, NULL)"; + $where[] = "(table_main._tenant = :_tenant OR table_main._tenant IS NULL)"; } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; @@ -1938,7 +1936,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) } if ($this->sharedTables) { - $where[] = "table_main._tenant IN (:_tenant, NULL)"; + $where[] = "(table_main._tenant = :_tenant OR table_main._tenant IS NULL)"; } $sqlWhere = !empty($where) @@ -2008,7 +2006,7 @@ public function sum(string $collection, string $attribute, array $queries = [], } if ($this->sharedTables) { - $where[] = "table_main._tenant IN (:_tenant, NULL)"; + $where[] = "(table_main._tenant = :_tenant OR table_main._tenant IS NULL)"; } $sqlWhere = !empty($where) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index cc8261dae..a6d4a9b7b 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -76,6 +76,7 @@ public function delete(string $name): bool * @param array $attributes * @param array $indexes * @return bool + * @throws DuplicateException */ public function createCollection(string $name, array $attributes = [], array $indexes = []): bool { @@ -85,8 +86,6 @@ public function createCollection(string $name, array $attributes = [], array $in /** @var array $attributeStrings */ $attributeStrings = []; - $this->getPDO()->beginTransaction(); - /** @var array $attributeStrings */ $attributeStrings = []; foreach ($attributes as $attribute) { @@ -99,13 +98,30 @@ public function createCollection(string $name, array $attributes = [], array $in $attribute->getAttribute('array', false) ); + // Ignore relationships with virtual attributes + if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { + $options = $attribute->getAttribute('options', []); + $relationType = $options['relationType'] ?? null; + $twoWay = $options['twoWay'] ?? false; + $side = $options['side'] ?? null; + + if ( + $relationType === Database::RELATION_MANY_TO_MANY + || ($relationType === Database::RELATION_ONE_TO_ONE && !$twoWay && $side === Database::RELATION_SIDE_CHILD) + || ($relationType === Database::RELATION_ONE_TO_MANY && $side === Database::RELATION_SIDE_PARENT) + || ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_CHILD) + ) { + continue; + } + } + $attributeStrings[] = "\"{$attrId}\" {$attrType}, "; } $sqlTenant = $this->sharedTables ? '_tenant INTEGER DEFAULT NULL,' : ''; - $sql = " - CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id)} ( + $collection = " + CREATE TABLE {$this->getSQLTable($id)} ( _id SERIAL NOT NULL, _uid VARCHAR(255) NOT NULL, ". $sqlTenant ." @@ -116,59 +132,60 @@ public function createCollection(string $name, array $attributes = [], array $in PRIMARY KEY (_id) ); "; + if ($this->sharedTables) { - $sql .= " + $collection .= " 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 .= " + $collection .= " 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); + $collection = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collection); - try { - $stmt->execute(); + $permissions = " + CREATE TABLE {$this->getSQLTable($id . '_perms')} ( + _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) + ); + "; - $sql = " - CREATE TABLE IF NOT EXISTS {$this->getSQLTable($id . '_perms')} ( - _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->sharedTables) { + $permissions .= " + 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 { + $permissions .= " + 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); + "; + } - if ($this->sharedTables) { - $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); - "; - } + $permissions = $this->trigger(Database::EVENT_COLLECTION_CREATE, $permissions); - $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + try { + $this->getPDO() + ->prepare($collection) + ->execute(); $this->getPDO() - ->prepare($sql) + ->prepare($permissions) ->execute(); foreach ($indexes as $index) { @@ -186,13 +203,16 @@ public function createCollection(string $name, array $attributes = [], array $in $indexOrders ); } - } catch (Exception $e) { - $this->getPDO()->rollBack(); - throw new DatabaseException('Failed to create collection: ' . $e->getMessage()); - } + } catch (PDOException $e) { + $e = $this->processException($e); - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); + if (!($e instanceof DuplicateException)) { + $this->getPDO() + ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->execute(); + } + + throw $e; } return true; @@ -900,11 +920,12 @@ public function createDocuments(string $collection, array $documents, int $batch return $documents; } - $this->getPDO()->beginTransaction(); try { + $this->getPDO()->beginTransaction(); $name = $this->filter($collection); $batches = \array_chunk($documents, max(1, $batchSize)); + $internalIds = []; foreach ($batches as $batch) { $bindIndex = 0; @@ -919,6 +940,11 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); + if(!empty($document->getInternalId())) { + $internalIds[$document->getId()] = true; + $attributes['_id'] = $document->getInternalId(); + } + if($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -978,9 +1004,6 @@ public function createDocuments(string $collection, array $documents, int $batch if (!$this->getPDO()->commit()) { throw new DatabaseException('Failed to commit transaction'); } - - return $documents; - } catch (PDOException $e) { $this->getPDO()->rollBack(); @@ -989,6 +1012,18 @@ public function createDocuments(string $collection, array $documents, int $batch default => $e, }; } + + foreach ($documents as $document) { + if(!isset($internalIds[$document->getId()])) { + $document['$internalId'] = $this->getDocument( + $collection, + $document->getId(), + [Query::select(['$internalId'])] + )->getInternalId(); + } + } + + return $documents; } /** @@ -1709,7 +1744,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if ($this->sharedTables) { - $where[] = "table_main._tenant IN (:_tenant, NULL)"; + $where[] = "(table_main._tenant = :_tenant OR table_main._tenant IS NULL)"; } if (Authorization::$status) { @@ -1835,7 +1870,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) } if ($this->sharedTables) { - $where[] = "table_main._tenant IN (:_tenant, NULL)"; + $where[] = "(table_main._tenant = :_tenant OR table_main._tenant IS NULL)"; } if (Authorization::$status) { @@ -1898,7 +1933,7 @@ public function sum(string $collection, string $attribute, array $queries = [], } if ($this->sharedTables) { - $where[] = "table_main._tenant IN (:_tenant, NULL)"; + $where[] = "(table_main._tenant = :_tenant OR table_main._tenant IS NULL)"; } if (Authorization::$status) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index e1bb58beb..3ec041afe 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1001,18 +1001,18 @@ public function testQueryTimeout(): void ])); } - $this->expectException(TimeoutException::class); - static::getDatabase()->setTimeout(1); try { static::getDatabase()->find('global-timeouts', [ Query::notEqual('longtext', 'appwrite'), ]); - } catch(TimeoutException $ex) { + $this->fail('Failed to throw exception'); + } catch(\Exception $e) { + \var_dump($e); + $this->assertInstanceOf(TimeoutException::class, $e); static::getDatabase()->clearTimeout(); static::getDatabase()->deleteCollection('global-timeouts'); - throw $ex; } } From 9f765890c403c8300c917a2248ce416a659971a6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 18:24:27 +1200 Subject: [PATCH 057/256] Update timeout test --- tests/e2e/Adapter/Base.php | 52 ++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 3ec041afe..a2aea9b30 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -986,37 +986,35 @@ public function testCreatedAtUpdatedAt(): void public function testQueryTimeout(): void { - if ($this->getDatabase()->getAdapter()->getSupportForTimeouts()) { - static::getDatabase()->createCollection('global-timeouts'); - $this->assertEquals(true, static::getDatabase()->createAttribute('global-timeouts', 'longtext', Database::VAR_STRING, 100000000, true)); + if (!$this->getDatabase()->getAdapter()->getSupportForTimeouts()) { + $this->expectNotToPerformAssertions(); + } + static::getDatabase()->createCollection('global-timeouts'); + $this->assertEquals(true, static::getDatabase()->createAttribute('global-timeouts', 'longtext', Database::VAR_STRING, 100000000, true)); - for ($i = 0 ; $i <= 20 ; $i++) { - static::getDatabase()->createDocument('global-timeouts', new Document([ - 'longtext' => file_get_contents(__DIR__ . '/../../resources/longtext.txt'), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()) - ] - ])); - } + for ($i = 0 ; $i <= 200 ; $i++) { + static::getDatabase()->createDocument('global-timeouts', new Document([ + 'longtext' => file_get_contents(__DIR__ . '/../../resources/longtext.txt'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ] + ])); + } - static::getDatabase()->setTimeout(1); + static::getDatabase()->setTimeout(1); - try { - static::getDatabase()->find('global-timeouts', [ - Query::notEqual('longtext', 'appwrite'), - ]); - $this->fail('Failed to throw exception'); - } catch(\Exception $e) { - \var_dump($e); - $this->assertInstanceOf(TimeoutException::class, $e); - static::getDatabase()->clearTimeout(); - static::getDatabase()->deleteCollection('global-timeouts'); - } + try { + static::getDatabase()->find('global-timeouts', [ + Query::notEqual('longtext', 'appwrite'), + ]); + $this->fail('Failed to throw exception'); + } catch(\Exception $e) { + static::getDatabase()->clearTimeout(); + static::getDatabase()->deleteCollection('global-timeouts'); + $this->assertInstanceOf(TimeoutException::class, $e); } - - $this->expectNotToPerformAssertions(); } /** From 0ba61b1db5ac28c63a3154ac3a37eb4174282a8b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 18:25:20 +1200 Subject: [PATCH 058/256] Fix dump output truncation --- dev/xdebug.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev/xdebug.ini b/dev/xdebug.ini index ddd00b1a2..34ec0e4b6 100644 --- a/dev/xdebug.ini +++ b/dev/xdebug.ini @@ -7,3 +7,7 @@ xdebug.client_host=host.docker.internal xdebug.client_port = 9003 xdebug.log = /tmp/xdebug.log xdebug.use_compression=false + +xdebug.var_display_max_depth = 10 +xdebug.var_display_max_children = 256 +xdebug.var_display_max_data = 4096 From ab549c157bbfadbfad0ab1144fd0ea3953f30170 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 18:26:02 +1200 Subject: [PATCH 059/256] Fix timeouts not thrown --- src/Database/Adapter/MariaDB.php | 3 +-- src/Database/Adapter/Mongo.php | 2 +- src/Database/Adapter/Postgres.php | 6 +++--- tests/e2e/Adapter/Base.php | 15 +++++++++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 7aed62094..e85cd236c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -314,8 +314,7 @@ public function createAttribute(string $collection, string $id, string $type, in ->prepare($sql) ->execute(); } catch (PDOException $e) { - $this->processException($e); - return false; + throw $this->processException($e); } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 8a8706a58..ba84b942a 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1059,7 +1059,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, try { $results = $this->client->find($name, $filters, $options)->cursor->firstBatch ?? []; } catch (MongoException $e) { - $this->processException($e); + throw $this->processException($e); } if (empty($results)) { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index a6d4a9b7b..bb75b23c5 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -300,8 +300,7 @@ public function createAttribute(string $collection, string $id, string $type, in ->prepare($sql) ->execute(); } catch (PDOException $e) { - $this->processException($e); - return false; + throw $this->processException($e); } } @@ -1661,6 +1660,7 @@ public function deleteDocument(string $collection, string $id): bool * @return array * @throws DatabaseException * @throws TimeoutException + * @throws Exception */ public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array { @@ -1804,7 +1804,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, try { $stmt->execute(); } catch (PDOException $e) { - $this->processException($e); + throw $this->processException($e); } $results = $stmt->fetchAll(); diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a2aea9b30..eabcb5d0d 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -989,10 +989,21 @@ public function testQueryTimeout(): void if (!$this->getDatabase()->getAdapter()->getSupportForTimeouts()) { $this->expectNotToPerformAssertions(); } + static::getDatabase()->createCollection('global-timeouts'); - $this->assertEquals(true, static::getDatabase()->createAttribute('global-timeouts', 'longtext', Database::VAR_STRING, 100000000, true)); - for ($i = 0 ; $i <= 200 ; $i++) { + $this->assertEquals( + true, + static::getDatabase()->createAttribute( + collection: 'global-timeouts', + id: 'longtext', + type: Database::VAR_STRING, + size: 100000000, + required: true + ) + ); + + for ($i = 0 ; $i <= 20 ; $i++) { static::getDatabase()->createDocument('global-timeouts', new Document([ 'longtext' => file_get_contents(__DIR__ . '/../../resources/longtext.txt'), '$permissions' => [ From 9fff3282750bc212291c71799f5e3dcbe850e5b9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 19:22:04 +1200 Subject: [PATCH 060/256] Fix SQLite tests --- src/Database/Adapter/SQLite.php | 98 +++++++++++++++++---------------- tests/e2e/Adapter/Base.php | 11 +++- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index aa971c98e..66b49173b 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -110,12 +110,6 @@ public function createCollection(string $name, array $attributes = [], array $in { $id = $this->filter($name); - try { - $this->getPDO()->beginTransaction(); - } catch (PDOException $e) { - $this->getPDO()->rollBack(); - } - /** @var array $attributeStrings */ $attributeStrings = []; @@ -134,8 +128,8 @@ public function createCollection(string $name, array $attributes = [], array $in $tenantQuery = $this->sharedTables ? '`_tenant` INTEGER DEFAULT NULL,' : ''; - $sql = " - CREATE TABLE IF NOT EXISTS `{$this->getSQLTable($id)}` ( + $collection = " + CREATE TABLE {$this->getSQLTable($id)} ( `_id` INTEGER PRIMARY KEY AUTOINCREMENT, `_uid` VARCHAR(36) NOT NULL, {$tenantQuery} @@ -146,32 +140,10 @@ public function createCollection(string $name, array $attributes = [], array $in ) "; - $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); - - $this->getPDO()->prepare($sql)->execute(); + $collection = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collection); - $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'], [], []); - - if ($this->sharedTables) { - $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], []); - } - - foreach ($indexes as $index) { - $indexId = $this->filter($index->getId()); - $indexType = $index->getAttribute('type'); - $indexAttributes = $index->getAttribute('attributes', []); - $indexLengths = $index->getAttribute('lengths', []); - $indexOrders = $index->getAttribute('orders', []); - - $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders); - } - - $tenantQuery = $this->sharedTables ? '`_tenant` INTEGER DEFAULT NULL,' : ''; - - $sql = " - CREATE TABLE IF NOT EXISTS `{$this->getSQLTable($id)}_perms` ( + $permissions = " + CREATE TABLE {$this->getSQLTable($id . '_perms')} ( `_id` INTEGER PRIMARY KEY AUTOINCREMENT, {$tenantQuery} `_type` VARCHAR(12) NOT NULL, @@ -180,18 +152,49 @@ public function createCollection(string $name, array $attributes = [], array $in ) "; - $sql = $this->trigger(Database::EVENT_COLLECTION_CREATE, $sql); + $permissions = $this->trigger(Database::EVENT_COLLECTION_CREATE, $permissions); - $this->getPDO() - ->prepare($sql) - ->execute(); + try { + $this->getPDO() + ->prepare($collection) + ->execute(); - $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); - $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []); + $this->getPDO() + ->prepare($permissions) + ->execute(); - $this->getPDO()->commit(); + $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'], [], []); - // Update $this->getCountOfIndexes when adding another default index + if ($this->sharedTables) { + $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], []); + } + + foreach ($indexes as $index) { + $indexId = $this->filter($index->getId()); + $indexType = $index->getAttribute('type'); + $indexAttributes = $index->getAttribute('attributes', []); + $indexLengths = $index->getAttribute('lengths', []); + $indexOrders = $index->getAttribute('orders', []); + + $this->createIndex($id, $indexId, $indexType, $indexAttributes, $indexLengths, $indexOrders); + } + + $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); + $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []); + + } catch (PDOException $e) { + $e = $this->processException($e); + + if (!($e instanceof Duplicate)) { + $this->getPDO() + ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->execute(); + } + + throw $e; + } return true; } @@ -252,14 +255,14 @@ public function deleteCollection(string $id): bool $this->getPDO()->rollBack(); } - $sql = "DROP TABLE IF EXISTS `{$this->getSQLTable($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 IF EXISTS `{$this->getSQLTable($id)}_perms`"; + $sql = "DROP TABLE IF EXISTS {$this->getSQLTable($id . '_perms')}"; $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); $this->getPDO() @@ -327,10 +330,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa } } - $sql = " - ALTER TABLE {$this->getSQLTable($name)} - DROP COLUMN `{$id}` - "; + $sql = "ALTER TABLE {$this->getSQLTable($name)} DROP COLUMN `{$id}`"; $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); @@ -1226,7 +1226,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): */ protected function getSQLTable(string $name): string { - return "{$this->getNamespace()}_{$name}"; + return "`{$this->getNamespace()}_{$name}`"; } /** @@ -1394,12 +1394,14 @@ protected function processException(PDOException $e): \Exception * PDO and Swoole PDOProxy swap error codes and errorInfo */ + // Timeout if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { return new TimeoutException($e->getMessage(), $e->getCode(), $e); } elseif ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { return new TimeoutException($e->getMessage(), $e->getCode(), $e); } + // Duplicate if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); } elseif ($e->getCode() === 1 && isset($e->errorInfo[0]) && $e->errorInfo[0] === 'HY000') { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index eabcb5d0d..25ffd1798 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -984,10 +984,19 @@ public function testCreatedAtUpdatedAt(): void } + /** + * @throws AuthorizationException + * @throws DuplicateException + * @throws ConflictException + * @throws LimitException + * @throws StructureException + * @throws DatabaseException + */ public function testQueryTimeout(): void { if (!$this->getDatabase()->getAdapter()->getSupportForTimeouts()) { $this->expectNotToPerformAssertions(); + return; } static::getDatabase()->createCollection('global-timeouts'); @@ -1003,7 +1012,7 @@ public function testQueryTimeout(): void ) ); - for ($i = 0 ; $i <= 20 ; $i++) { + for ($i = 0; $i < 20; $i++) { static::getDatabase()->createDocument('global-timeouts', new Document([ 'longtext' => file_get_contents(__DIR__ . '/../../resources/longtext.txt'), '$permissions' => [ From fd1bc7f67630db5228b5ad715f0d714c90cb98d6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 19:24:09 +1200 Subject: [PATCH 061/256] Fix Mirror tests --- src/Database/Mirror.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index ecbcc329f..4f212fd0c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -367,7 +367,8 @@ public function updateAttribute(string $collection, string $id, string $type = n $array, $format, $formatOptions, - $filters + $filters, + $newKey, ); if ($this->destination === null) { @@ -396,7 +397,8 @@ public function updateAttribute(string $collection, string $id, string $type = n $document->getAttribute('array'), $document->getAttribute('format'), $document->getAttribute('formatOptions'), - $document->getAttribute('filters') + $document->getAttribute('filters'), + $newKey, ); } catch (\Throwable $err) { $this->logError('updateAttribute', $err); From 8b169a4ceb2de4e4fb75ce50ca04e32f5cdbffaa Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 20:54:39 +1200 Subject: [PATCH 062/256] Fix SQLite tests --- tests/e2e/Adapter/Base.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 25ffd1798..718d91118 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -62,16 +62,11 @@ public function testPing(): void public function testCreateExistsDelete(): void { - $schemaSupport = $this->getDatabase()->getAdapter()->getSupportForSchemas(); - if (!$schemaSupport) { - $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); - $this->assertEquals(true, static::getDatabase()->create()); + if (!static::getDatabase()->getAdapter()->getSupportForSchemas()) { + $this->expectNotToPerformAssertions(); return; } - if (!static::getDatabase()->exists($this->testDatabase)) { - $this->assertEquals(true, static::getDatabase()->create()); - } $this->assertEquals(true, static::getDatabase()->exists($this->testDatabase)); $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); $this->assertEquals(false, static::getDatabase()->exists($this->testDatabase)); From 088d549407657e690877386d34e9d813d68f3472 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 21:03:03 +1200 Subject: [PATCH 063/256] Fix missing silent calls in mirror --- src/Database/Mirror.php | 29 ++++++++++++++++------------- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 4f212fd0c..aec8beb0c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -226,13 +226,15 @@ public function createCollection(string $id, array $attributes = [], array $inde $documentSecurity ); - $this->createUpgrades(); - - $this->source->createDocument('upgrades', new Document([ - '$id' => $id, - 'collectionId' => $id, - 'status' => 'upgraded' - ])); + $this->silent(function () use ($id) { + $this->createUpgrades(); + + $this->source->createDocument('upgrades', new Document([ + '$id' => $id, + 'collectionId' => $id, + 'status' => 'upgraded' + ])); + }); } catch (\Throwable $err) { $this->logError('createCollection', $err); } @@ -512,7 +514,7 @@ public function createDocument(string $collection, Document $document): Document return $document; } - $upgrade = $this->getUpgradeStatus($collection); + $upgrade = $this->silent(fn() => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $document; } @@ -562,7 +564,7 @@ public function createDocuments( return $documents; } - $upgrade = $this->getUpgradeStatus($collection); + $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $documents; } @@ -618,7 +620,8 @@ public function updateDocument(string $collection, string $id, Document $documen return $document; } - $upgrade = $this->getUpgradeStatus($collection); + $upgrade = $this->silent(fn() => $this->getUpgradeStatus($collection)); + if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $document; } @@ -668,7 +671,7 @@ public function updateDocuments( return $documents; } - $upgrade = $this->getUpgradeStatus($collection); + $upgrade = $this->silent(fn() => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $documents; } @@ -723,7 +726,7 @@ public function deleteDocument(string $collection, string $id): bool return $result; } - $upgrade = $this->getUpgradeStatus($collection); + $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $result; } @@ -900,7 +903,7 @@ protected function getUpgradeStatus(string $collection): ?Document try { return $this->getDocument('upgrades', $collection); } catch (\Throwable) { - return; + return null; } }); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 718d91118..9d97ec0bf 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15392,7 +15392,7 @@ public function testEvents(): void $database->setDatabase('hellodb'); $database->create(); } else { - array_shift($events); + \array_shift($events); } $database->list(); From bad0ea537637fff0546287c92e4a3242cd129b8d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 19 Aug 2024 21:05:54 +1200 Subject: [PATCH 064/256] Lint --- src/Database/Mirror.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index aec8beb0c..6ff3860a7 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -514,7 +514,7 @@ public function createDocument(string $collection, Document $document): Document return $document; } - $upgrade = $this->silent(fn() => $this->getUpgradeStatus($collection)); + $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $document; } @@ -620,7 +620,7 @@ public function updateDocument(string $collection, string $id, Document $documen return $document; } - $upgrade = $this->silent(fn() => $this->getUpgradeStatus($collection)); + $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $document; @@ -671,7 +671,7 @@ public function updateDocuments( return $documents; } - $upgrade = $this->silent(fn() => $this->getUpgradeStatus($collection)); + $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { return $documents; } @@ -903,7 +903,7 @@ protected function getUpgradeStatus(string $collection): ?Document try { return $this->getDocument('upgrades', $collection); } catch (\Throwable) { - return null; + return; } }); } From 67bf1fef4dff6503b897a905bdb5fbd200587e6b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 21 Aug 2024 20:11:14 +1200 Subject: [PATCH 065/256] Merge pull request #439 from utopia-php/feat-atomic-updates Feat atomic updates # Conflicts: # composer.json # composer.lock # src/Database/Adapter/MariaDB.php # src/Database/Adapter/Postgres.php # src/Database/Adapter/SQLite.php # src/Database/Database.php # tests/e2e/Adapter/Base.php --- composer.json | 16 +- composer.lock | 46 ++-- src/Database/Adapter.php | 71 ++++- src/Database/Adapter/MariaDB.php | 50 +--- src/Database/Adapter/Mongo.php | 22 +- src/Database/Adapter/Postgres.php | 104 ++------ src/Database/Adapter/SQL.php | 98 ++++++- src/Database/Adapter/SQLite.php | 49 +--- src/Database/Database.php | 421 ++++++++++++++++++------------ 9 files changed, 503 insertions(+), 374 deletions(-) diff --git a/composer.json b/composer.json index 52f8a44f8..d0097c5d5 100755 --- a/composer.json +++ b/composer.json @@ -41,14 +41,14 @@ "utopia-php/mongo": "0.3.*" }, "require-dev": { - "fakerphp/faker": "^1.14", - "phpunit/phpunit": "^9.4", - "pcov/clobber": "^2.0", - "swoole/ide-helper": "5.1.2", - "utopia-php/cli": "^0.14.0", - "laravel/pint": "1.13.*", - "phpstan/phpstan": "1.10.*", - "rregeer/phpunit-coverage-check": "^0.3.1" + "fakerphp/faker": "1.23.*", + "phpunit/phpunit": "9.6.*", + "pcov/clobber": "2.0.*", + "swoole/ide-helper": "5.1.3", + "utopia-php/cli": "0.14.*", + "laravel/pint": "1.17.*", + "phpstan/phpstan": "1.11.*", + "rregeer/phpunit-coverage-check": "0.3.*" }, "suggests": { "ext-mongodb": "Needed to support MongoDB Database Adapter", diff --git a/composer.lock b/composer.lock index 51c48d890..5bd08c299 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1ef7c11d9a0628b3c57c8c80dbebbba1", + "content-hash": "8a6537ec5c4f47cd0b6c34e9ceda042b", "packages": [ { "name": "jean85/pretty-package-versions", @@ -506,16 +506,16 @@ }, { "name": "laravel/pint", - "version": "v1.13.11", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "60a163c3e7e3346a1dec96d3e6f02e6465452552" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/60a163c3e7e3346a1dec96d3e6f02e6465452552", - "reference": "60a163c3e7e3346a1dec96d3e6f02e6465452552", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -526,13 +526,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.49.0", - "illuminate/view": "^10.43.0", - "larastan/larastan": "^2.8.1", - "laravel-zero/framework": "^10.3.0", - "mockery/mockery": "^1.6.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", + "laravel-zero/framework": "^10.4.0", + "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.33.6" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -568,7 +568,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-02-13T17:20:13+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "myclabs/deep-copy", @@ -840,16 +840,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.67", + "version": "1.11.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" + "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/707c2aed5d8d0075666e673a5e71440c1d01a5a3", + "reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3", "shasum": "" }, "require": { @@ -894,7 +894,7 @@ "type": "github" } ], - "time": "2024-04-16T07:22:02+00:00" + "time": "2024-08-19T14:37:29+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2382,16 +2382,16 @@ }, { "name": "swoole/ide-helper", - "version": "5.1.2", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/swoole/ide-helper.git", - "reference": "33ec7af9111b76d06a70dd31191cc74793551112" + "reference": "9cfc6669b83be0fa6fface91a6f372a0bb84bf1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swoole/ide-helper/zipball/33ec7af9111b76d06a70dd31191cc74793551112", - "reference": "33ec7af9111b76d06a70dd31191cc74793551112", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/9cfc6669b83be0fa6fface91a6f372a0bb84bf1a", + "reference": "9cfc6669b83be0fa6fface91a6f372a0bb84bf1a", "shasum": "" }, "type": "library", @@ -2408,9 +2408,9 @@ "description": "IDE help files for Swoole.", "support": { "issues": "https://github.com/swoole/ide-helper/issues", - "source": "https://github.com/swoole/ide-helper/tree/5.1.2" + "source": "https://github.com/swoole/ide-helper/tree/5.1.3" }, - "time": "2024-02-01T22:28:11+00:00" + "time": "2024-06-17T05:45:20+00:00" }, { "name": "symfony/deprecation-contracts", diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 00d8436bf..05d651c8d 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -17,6 +17,8 @@ abstract class Adapter protected ?int $tenant = null; + protected int $inTransaction = 0; + /** * @var array */ @@ -232,6 +234,70 @@ public function resetMetadata(): static return $this; } + /** + * Start a new transaction. + * + * If a transaction is already active, this will only increment the transaction count and return true. + * + * @return bool + * @throws DatabaseException + */ + abstract public function startTransaction(): bool; + + /** + * Commit a transaction. + * + * If no transaction is active, this will be a no-op and will return false. + * If there is more than one active transaction, this decrement the transaction count and return true. + * If the transaction count is 1, it will be commited, the transaction count will be reset to 0, and return true. + * + * @return bool + * @throws DatabaseException + */ + abstract public function commitTransaction(): bool; + + /** + * Rollback a transaction. + * + * If no transaction is active, this will be a no-op and will return false. + * If 1 or more transactions are active, this will roll back all transactions, reset the count to 0, and return true. + * + * @return bool + * @throws DatabaseException + */ + abstract public function rollbackTransaction(): bool; + + /** + * Check if a transaction is active. + * + * @return bool + * @throws DatabaseException + */ + public function inTransaction(): bool + { + return $this->inTransaction > 0; + } + + /** + * @template T + * @param callable(): T $callback + * @return T + * @throws \Throwable + */ + public function withTransaction(callable $callback): mixed + { + $this->startTransaction(); + + try { + $result = $callback(); + $this->commitTransaction(); + return $result; + } catch (\Throwable $e) { + $this->rollbackTransaction(); + throw $e; + } + } + /** * Apply a transformation to a query before an event occurs * @@ -460,9 +526,10 @@ abstract public function deleteIndex(string $collection, string $id): bool; * @param string $collection * @param string $id * @param array $queries + * @param bool $forUpdate * @return Document */ - abstract public function getDocument(string $collection, string $id, array $queries = []): Document; + abstract public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): Document; /** * Create Document @@ -669,6 +736,8 @@ abstract public function getSupportForTimeouts(): bool; */ abstract public function getSupportForRelationships(): bool; + abstract public function getSupportForUpdateLock(): bool; + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e85cd236c..b7ba2ff6c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -772,8 +772,6 @@ public function deleteIndex(string $collection, string $id): bool public function createDocument(string $collection, Document $document): Document { try { - $this->getPDO()->beginTransaction(); - $attributes = $document->getAttributes(); $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); @@ -879,15 +877,7 @@ public function createDocument(string $collection, Document $document): Document if (isset($stmtPermissions)) { $stmtPermissions->execute(); } - - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } } catch (\Throwable $e) { - if($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } - if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: @@ -921,7 +911,6 @@ public function createDocuments(string $collection, array $documents, int $batch } try { - $this->getPDO()->beginTransaction(); $name = $this->filter($collection); $batches = \array_chunk($documents, \max(1, $batchSize)); $internalIds = []; @@ -1019,15 +1008,7 @@ public function createDocuments(string $collection, array $documents, int $batch $stmtPermissions?->execute(); } } - - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } } catch (\Throwable $e) { - if($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } - if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: @@ -1066,8 +1047,6 @@ public function createDocuments(string $collection, array $documents, int $batch public function updateDocument(string $collection, Document $document): Document { try { - $this->getPDO()->beginTransaction(); - $attributes = $document->getAttributes(); $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); @@ -1090,6 +1069,8 @@ public function updateDocument(string $collection, Document $document): Document $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } + $sql .= ' FOR UPDATE'; + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); /** @@ -1285,14 +1266,7 @@ public function updateDocument(string $collection, Document $document): Document $stmtAddPermissions->execute(); } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } } catch (\Throwable $e) { - if($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } - if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: @@ -1326,8 +1300,6 @@ public function updateDocuments(string $collection, array $documents, int $batch } try { - $this->getPDO()->beginTransaction(); - $name = $this->filter($collection); $batches = \array_chunk($documents, max(1, $batchSize)); @@ -1551,15 +1523,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $stmtAddPermissions->execute(); } } - - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } } catch (\Throwable $e) { - if($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } - if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: @@ -1636,8 +1600,6 @@ public function increaseDocumentAttribute(string $collection, string $id, string public function deleteDocument(string $collection, string $id): bool { try { - $this->getPDO()->beginTransaction(); - $name = $this->filter($collection); $sql = " @@ -1686,15 +1648,7 @@ public function deleteDocument(string $collection, string $id): bool if (!$stmtPermissions->execute()) { throw new DatabaseException('Failed to delete permissions'); } - - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } } catch (\Throwable $e) { - if($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } - throw new DatabaseException($e->getMessage(), $e->getCode(), $e); } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index ba84b942a..9e48c9a41 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -58,6 +58,21 @@ public function __construct(Client $client) $this->client->connect(); } + public function startTransaction(): bool + { + return true; + } + + public function commitTransaction(): bool + { + return true; + } + + public function rollbackTransaction(): bool + { + return true; + } + /** * Ping Database * @@ -625,7 +640,7 @@ public function deleteIndex(string $collection, string $id): bool * @return Document * @throws MongoException */ - public function getDocument(string $collection, string $id, array $queries = []): Document + public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): Document { $name = $this->getNamespace() . '_' . $this->filter($collection); @@ -1641,6 +1656,11 @@ public function getSupportForRelationships(): bool return false; } + public function getSupportForUpdateLock(): bool + { + return false; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index bb75b23c5..edbb00203 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -374,61 +374,47 @@ public function renameAttribute(string $collection, string $old, string $new): b */ public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, string $newKey = null): bool { - try { - $name = $this->filter($collection); - $id = $this->filter($id); - $type = $this->getSQLType($type, $size, $signed, $array); - - $this->getPDO()->beginTransaction(); + $name = $this->filter($collection); + $id = $this->filter($id); + $type = $this->getSQLType($type, $size, $signed, $array); - if ($type == 'TIMESTAMP(3)') { - $type = "TIMESTAMP(3) without time zone USING TO_TIMESTAMP(\"$id\", 'YYYY-MM-DD HH24:MI:SS.MS')"; - } + if ($type == 'TIMESTAMP(3)') { + $type = "TIMESTAMP(3) without time zone USING TO_TIMESTAMP(\"$id\", 'YYYY-MM-DD HH24:MI:SS.MS')"; + } - if (!empty($newKey) && $id !== $newKey) { - $newKey = $this->filter($newKey); + if (!empty($newKey) && $id !== $newKey) { + $newKey = $this->filter($newKey); - $sql = " + $sql = " ALTER TABLE {$this->getSQLTable($name)} RENAME COLUMN \"{$id}\" TO \"{$newKey}\" "; - $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - - $result = $this->getPDO() - ->prepare($sql) - ->execute(); + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - if (!$result) { - return false; - } + $result = $this->getPDO() + ->prepare($sql) + ->execute(); - $id = $newKey; + if (!$result) { + return false; } - $sql = " + $id = $newKey; + } + + $sql = " ALTER TABLE {$this->getSQLTable($name)} ALTER COLUMN \"{$id}\" TYPE {$type} "; - $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - - $result = $this->getPDO() - ->prepare($sql) - ->execute(); - - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - return $result; - } catch (\Throwable $e) { - if($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } + $result = $this->getPDO() + ->prepare($sql) + ->execute(); - throw $e; - } + return $result; } /** @@ -801,8 +787,6 @@ public function createDocument(string $collection, Document $document): Document $columns = ''; $columnNames = ''; - $this->getPDO()->beginTransaction(); - /** * Insert Attributes */ @@ -888,17 +872,12 @@ public function createDocument(string $collection, Document $document): Document } catch (Throwable $e) { switch ($e->getCode()) { case 23505: - $this->getPDO()->rollBack(); - throw new DuplicateException('Duplicated document: ' . $e->getMessage()); + throw new Duplicate('Duplicated document: ' . $e->getMessage()); default: throw $e; } } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return $document; } @@ -919,9 +898,7 @@ public function createDocuments(string $collection, array $documents, int $batch return $documents; } - try { - $this->getPDO()->beginTransaction(); $name = $this->filter($collection); $batches = \array_chunk($documents, max(1, $batchSize)); $internalIds = []; @@ -1000,12 +977,8 @@ public function createDocuments(string $collection, array $documents, int $batch } } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } + return $documents; } catch (PDOException $e) { - $this->getPDO()->rollBack(); - throw match ($e->getCode()) { 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), default => $e, @@ -1059,6 +1032,8 @@ public function updateDocument(string $collection, Document $document): Document $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } + $sql .= ' FOR UPDATE'; + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); /** @@ -1086,8 +1061,6 @@ public function updateDocument(string $collection, Document $document): Document return $carry; }, $initial); - $this->getPDO()->beginTransaction(); - /** * Get removed Permissions */ @@ -1242,9 +1215,6 @@ public function updateDocument(string $collection, Document $document): Document $stmtAddPermissions->execute(); } } catch (PDOException $e) { - $this->getPDO()->rollBack(); - - // Must be a switch for loose match switch ($e->getCode()) { case 1062: case 23505: @@ -1254,10 +1224,6 @@ public function updateDocument(string $collection, Document $document): Document } } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return $document; } @@ -1277,8 +1243,6 @@ public function updateDocuments(string $collection, array $documents, int $batch return $documents; } - $this->getPDO()->beginTransaction(); - try { $name = $this->filter($collection); $batches = \array_chunk($documents, max(1, $batchSize)); @@ -1501,13 +1465,8 @@ public function updateDocuments(string $collection, array $documents, int $batch } } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return $documents; } catch (PDOException $e) { - $this->getPDO()->rollBack(); // Must be a switch for loose match switch ($e->getCode()) { @@ -1582,8 +1541,6 @@ public function deleteDocument(string $collection, string $id): bool { $name = $this->filter($collection); - $this->getPDO()->beginTransaction(); - $sql = " DELETE FROM {$this->getSQLTable($name)} WHERE _uid = :_uid @@ -1632,14 +1589,9 @@ public function deleteDocument(string $collection, string $id): bool throw new DatabaseException('Failed to delete permissions'); } } catch (\Throwable $th) { - $this->getPDO()->rollBack(); throw new DatabaseException($th->getMessage()); } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return $deleted; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index de639036d..e115afdb3 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -27,6 +27,81 @@ public function __construct(mixed $pdo) $this->pdo = $pdo; } + /** + * @inheritDoc + */ + public function startTransaction(): bool + { + try { + if ($this->inTransaction === 0) { + $result = $this->getPDO()->beginTransaction(); + } else { + $result = true; + } + } catch (PDOException $e) { + throw new DatabaseException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); + } + + if (!$result) { + throw new DatabaseException('Failed to start transaction'); + } + + $this->inTransaction++; + + return $result; + } + + /** + * @inheritDoc + */ + public function commitTransaction(): bool + { + if ($this->inTransaction === 0) { + return false; + } elseif ($this->inTransaction > 1) { + $this->inTransaction--; + return true; + } + + try { + $result = $this->getPDO()->commit(); + } catch (PDOException $e) { + throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); + } finally { + $this->inTransaction--; + } + + if (!$result) { + throw new DatabaseException('Failed to commit transaction'); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function rollbackTransaction(): bool + { + if ($this->inTransaction === 0) { + return false; + } + + try { + $result = $this->getPDO()->rollBack(); + } catch (PDOException $e) { + throw new DatabaseException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); + } finally { + $this->inTransaction = 0; + } + + if (!$result) { + throw new DatabaseException('Failed to rollback transaction'); + } + + return $result; + } + /** * Ping Database * @@ -101,16 +176,19 @@ public function list(): array * @param string $collection * @param string $id * @param Query[] $queries + * @param bool $forUpdate * @return Document - * @throws Exception + * @throws DatabaseException */ - public function getDocument(string $collection, string $id, array $queries = []): Document + public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): Document { $name = $this->filter($collection); $selections = $this->getAttributeSelections($queries); + $forUpdate = $forUpdate ? 'FOR UPDATE' : ''; + $sql = " - SELECT {$this->getAttributeProjection($selections)} + SELECT {$this->getAttributeProjection($selections)} FROM {$this->getSQLTable($name)} WHERE _uid = :_uid "; @@ -119,6 +197,10 @@ public function getDocument(string $collection, string $id, array $queries = []) $sql .= "AND (_tenant = :_tenant OR _tenant IS NULL)"; } + if ($this->getSupportForUpdateLock()) { + $sql .= " {$forUpdate}"; + } + $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $id); @@ -259,6 +341,16 @@ public function getSupportForFulltextIndex(): bool return true; } + /** + * Are FOR UPDATE locks supported? + * + * @return bool + */ + public function getSupportForUpdateLock(): bool + { + return true; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 66b49173b..e9f4de36e 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -167,6 +167,9 @@ public function createCollection(string $name, array $attributes = [], array $in $this->createIndex($id, '_created_at', Database::INDEX_KEY, [ '_createdAt'], [], []); $this->createIndex($id, '_updated_at', Database::INDEX_KEY, [ '_updatedAt'], [], []); + $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); + $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission', '_type'], [], []); + if ($this->sharedTables) { $this->createIndex($id, '_tenant_id', Database::INDEX_KEY, [ '_id'], [], []); } @@ -249,13 +252,7 @@ public function deleteCollection(string $id): bool { $id = $this->filter($id); - try { - $this->getPDO()->beginTransaction(); - } catch (PDOException $e) { - $this->getPDO()->rollBack(); - } - - $sql = "DROP TABLE IF EXISTS {$this->getSQLTable($id)}"; + $sql = "DROP TABLE IF EXISTS `{$this->getSQLTable($id)}`"; $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); $this->getPDO() @@ -269,8 +266,6 @@ public function deleteCollection(string $id): bool ->prepare($sql) ->execute(); - $this->getPDO()->commit(); - return true; } @@ -472,12 +467,6 @@ public function createDocument(string $collection, Document $document): Document $columns = ['_uid']; $values = ['_uid']; - try { - $this->getPDO()->beginTransaction(); - } catch (PDOException $e) { - $this->getPDO()->rollBack(); - } - /** * Insert Attributes */ @@ -562,16 +551,12 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->execute(); } } catch (PDOException $e) { - $this->getPDO()->rollBack(); throw match ($e->getCode()) { "1062", "23000" => new Duplicate('Duplicated document: ' . $e->getMessage()), default => $e, }; } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } return $document; } @@ -600,7 +585,6 @@ public function updateDocument(string $collection, Document $document): Document $name = $this->filter($collection); $columns = ''; - $sql = " SELECT _type, _permission FROM `{$this->getNamespace()}_{$name}_perms` @@ -638,12 +622,6 @@ public function updateDocument(string $collection, Document $document): Document return $carry; }, $initial); - try { - $this->getPDO()->beginTransaction(); - } catch (PDOException $e) { - $this->getPDO()->rollBack(); - } - /** * Get removed Permissions */ @@ -798,8 +776,6 @@ public function updateDocument(string $collection, Document $document): Document $stmtAddPermissions->execute(); } } catch (PDOException $e) { - $this->getPDO()->rollBack(); - throw match ($e->getCode()) { '1062', '23000' => new Duplicate('Duplicated document: ' . $e->getMessage()), @@ -807,10 +783,6 @@ public function updateDocument(string $collection, Document $document): Document }; } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return $document; } @@ -831,8 +803,6 @@ public function updateDocuments(string $collection, array $documents, int $batch return $documents; } - $this->getPDO()->beginTransaction(); - try { $name = $this->filter($collection); $batches = \array_chunk($documents, max(1, $batchSize)); @@ -1061,14 +1031,8 @@ public function updateDocuments(string $collection, array $documents, int $batch } } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return $documents; } catch (PDOException $e) { - $this->getPDO()->rollBack(); - throw match ($e->getCode()) { 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), default => $e, @@ -1126,6 +1090,11 @@ public function getSupportForRelationships(): bool return false; } + public function getSupportForUpdateLock(): bool + { + return false; + } + /** * Get SQL Index Type * diff --git a/src/Database/Database.php b/src/Database/Database.php index fbf92c4eb..61b2f2b98 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -935,6 +935,59 @@ public function getAdapter(): Adapter return $this->adapter; } + /** + * Start a new transaction. + * + * If a transaction is already active, this will only increment the transaction count and return true. + * + * @return bool + * @throws DatabaseException + */ + public function startTransaction(): bool + { + return $this->adapter->startTransaction(); + } + + /** + * Commit a transaction. + * + * If no transaction is active, this will be a no-op and will return false. + * If there is more than one active transaction, this decrement the transaction count and return true. + * If the transaction count is 1, it will be commited, the transaction count will be reset to 0, and return true. + * + * @return bool + * @throws DatabaseException + */ + public function commitTransaction(): bool + { + return $this->adapter->startTransaction(); + } + + /** + * Rollback a transaction. + * + * If no transaction is active, this will be a no-op and will return false. + * If 1 or more transactions are active, this will roll back all transactions, reset the count to 0, and return true. + * + * @return bool + * @throws DatabaseException + */ + public function rollbackTransaction(): bool + { + return $this->adapter->rollbackTransaction(); + } + + /** + * @template T + * @param callable(): T $callback + * @return T + * @throws \Throwable + */ + public function withTransaction(callable $callback): mixed + { + return $this->adapter->withTransaction($callback); + } + /** * Ping Database * @@ -2659,7 +2712,7 @@ public function deleteIndex(string $collection, string $id): bool * @throws DatabaseException * @throws Exception */ - public function getDocument(string $collection, string $id, array $queries = []): Document + public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): Document { if ($collection === self::METADATA && $id === self::METADATA) { return new Document(self::COLLECTION); @@ -2763,7 +2816,7 @@ public function getDocument(string $collection, string $id, array $queries = []) return $document; } - $document = $this->adapter->getDocument($collection->getId(), $id, $queries); + $document = $this->adapter->getDocument($collection->getId(), $id, $queries, $forUpdate); if ($document->isEmpty()) { return $document; @@ -3138,11 +3191,13 @@ public function createDocument(string $collection, Document $document): Document throw new StructureException($structure->getDescription()); } - if ($this->resolveRelationships) { - $document = $this->silent(fn () => $this->createDocumentRelationships($collection, $document)); - } + $document = $this->withTransaction(function () use ($collection, $document) { + if ($this->resolveRelationships) { + $document = $this->silent(fn () => $this->createDocumentRelationships($collection, $document)); + } - $document = $this->adapter->createDocument($collection->getId(), $document); + return $this->adapter->createDocument($collection->getId(), $document); + }); if ($this->resolveRelationships) { $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); @@ -3202,7 +3257,9 @@ public function createDocuments(string $collection, array $documents, int $batch $documents[$key] = $document; } - $documents = $this->adapter->createDocuments($collection->getId(), $documents, $batchSize); + $documents = $this->withTransaction(function () use ($collection, $documents, $batchSize) { + return $this->adapter->createDocuments($collection->getId(), $documents, $batchSize); + }); foreach ($documents as $key => $document) { if ($this->resolveRelationships) { @@ -3559,165 +3616,172 @@ public function updateDocument(string $collection, string $id, Document $documen throw new DatabaseException('Must define $id attribute'); } - $time = DateTime::now(); - $old = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this + $collection = $this->silent(fn () => $this->getCollection($collection)); - $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); - $document['$createdAt'] = $old->getCreatedAt(); // Make sure user doesn't switch createdAt - $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID + $document = $this->withTransaction(function () use ($collection, $id, $document) { + $time = DateTime::now(); + $old = Authorization::skip(fn () => $this->silent( + fn () => + $this->getDocument($collection->getId(), $id, forUpdate: true) + )); - if($this->adapter->getSharedTables()) { - $document['$tenant'] = $old->getAttribute('$tenant'); // Make sure user doesn't switch tenant - } + $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); + $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID + $document['$createdAt'] = $old->getCreatedAt(); // Make sure user doesn't switch createdAt - $document = new Document($document); + if ($this->adapter->getSharedTables()) { + $document['$tenant'] = $old->getAttribute('$tenant'); // Make sure user doesn't switch tenant + } - $collection = $this->silent(fn () => $this->getCollection($collection)); + $document = new Document($document); - $relationships = \array_filter($collection->getAttribute('attributes', []), function ($attribute) { - return $attribute['type'] === Database::VAR_RELATIONSHIP; - }); + $relationships = \array_filter($collection->getAttribute('attributes', []), function ($attribute) { + return $attribute['type'] === Database::VAR_RELATIONSHIP; + }); - $updateValidator = new Authorization(self::PERMISSION_UPDATE); - $readValidator = new Authorization(self::PERMISSION_READ); - $shouldUpdate = false; + $updateValidator = new Authorization(self::PERMISSION_UPDATE); + $readValidator = new Authorization(self::PERMISSION_READ); + $shouldUpdate = false; - if ($collection->getId() !== self::METADATA) { - $documentSecurity = $collection->getAttribute('documentSecurity', false); + if ($collection->getId() !== self::METADATA) { + $documentSecurity = $collection->getAttribute('documentSecurity', false); - foreach ($relationships as $relationship) { - $relationships[$relationship->getAttribute('key')] = $relationship; - } + foreach ($relationships as $relationship) { + $relationships[$relationship->getAttribute('key')] = $relationship; + } - // Compare if the document has any changes - foreach ($document as $key => $value) { - // Skip the nested documents as they will be checked later in recursions. - if (\array_key_exists($key, $relationships)) { - // No need to compare nested documents more than max depth. - if (count($this->relationshipWriteStack) >= Database::RELATION_MAX_DEPTH - 1) { - continue; - } - $relationType = (string) $relationships[$key]['options']['relationType']; - $side = (string) $relationships[$key]['options']['side']; - switch ($relationType) { - case Database::RELATION_ONE_TO_ONE: - $oldValue = $old->getAttribute($key) instanceof Document - ? $old->getAttribute($key)->getId() - : $old->getAttribute($key); - - if ((\is_null($value) !== \is_null($oldValue)) - || (\is_string($value) && $value !== $oldValue) - || ($value instanceof Document && $value->getId() !== $oldValue) - ) { - $shouldUpdate = true; - } - break; - case Database::RELATION_ONE_TO_MANY: - case Database::RELATION_MANY_TO_ONE: - case Database::RELATION_MANY_TO_MANY: - if ( - ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_PARENT) || - ($relationType === Database::RELATION_ONE_TO_MANY && $side === Database::RELATION_SIDE_CHILD) - ) { + // Compare if the document has any changes + foreach ($document as $key => $value) { + // Skip the nested documents as they will be checked later in recursions. + if (\array_key_exists($key, $relationships)) { + // No need to compare nested documents more than max depth. + if (count($this->relationshipWriteStack) >= Database::RELATION_MAX_DEPTH - 1) { + continue; + } + $relationType = (string)$relationships[$key]['options']['relationType']; + $side = (string)$relationships[$key]['options']['side']; + switch ($relationType) { + case Database::RELATION_ONE_TO_ONE: $oldValue = $old->getAttribute($key) instanceof Document ? $old->getAttribute($key)->getId() : $old->getAttribute($key); if ((\is_null($value) !== \is_null($oldValue)) || (\is_string($value) && $value !== $oldValue) - || ($value instanceof Document && $value->getId() !== $oldValue) + || ($value instanceof Document && $value->getId() !== $oldValue) ) { $shouldUpdate = true; } break; - } - - if (!\is_array($value) || !\array_is_list($value)) { - throw new RelationshipException('Invalid relationship value. Must be either an array of documents or document IDs, ' . \gettype($value) . ' given.'); - } + case Database::RELATION_ONE_TO_MANY: + case Database::RELATION_MANY_TO_ONE: + case Database::RELATION_MANY_TO_MANY: + if ( + ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_PARENT) || + ($relationType === Database::RELATION_ONE_TO_MANY && $side === Database::RELATION_SIDE_CHILD) + ) { + $oldValue = $old->getAttribute($key) instanceof Document + ? $old->getAttribute($key)->getId() + : $old->getAttribute($key); - if (\count($old->getAttribute($key)) !== \count($value)) { - $shouldUpdate = true; - break; - } + if ((\is_null($value) !== \is_null($oldValue)) + || (\is_string($value) && $value !== $oldValue) + || ($value instanceof Document && $value->getId() !== $oldValue) + ) { + $shouldUpdate = true; + } + break; + } - foreach ($value as $index => $relation) { - $oldValue = $old->getAttribute($key)[$index] instanceof Document - ? $old->getAttribute($key)[$index]->getId() - : $old->getAttribute($key)[$index]; + if (!\is_array($value) || !\array_is_list($value)) { + throw new RelationshipException('Invalid relationship value. Must be either an array of documents or document IDs, ' . \gettype($value) . ' given.'); + } - if ( - (\is_string($relation) && $relation !== $oldValue) || - ($relation instanceof Document && $relation->getId() !== $oldValue) - ) { + if (\count($old->getAttribute($key)) !== \count($value)) { $shouldUpdate = true; break; } - } + + foreach ($value as $index => $relation) { + $oldValue = $old->getAttribute($key)[$index] instanceof Document + ? $old->getAttribute($key)[$index]->getId() + : $old->getAttribute($key)[$index]; + + if ( + (\is_string($relation) && $relation !== $oldValue) || + ($relation instanceof Document && $relation->getId() !== $oldValue) + ) { + $shouldUpdate = true; + break; + } + } + break; + } + + if ($shouldUpdate) { break; + } + + continue; } - if ($shouldUpdate) { + $oldValue = $old->getAttribute($key); + + // If values are not equal we need to update document. + if ($value !== $oldValue) { + $shouldUpdate = true; break; } - - continue; } - $oldValue = $old->getAttribute($key); + $updatePermissions = [ + ...$collection->getUpdate(), + ...($documentSecurity ? $old->getUpdate() : []) + ]; - // If values are not equal we need to update document. - if ($value !== $oldValue) { - $shouldUpdate = true; - break; + $readPermissions = [ + ...$collection->getRead(), + ...($documentSecurity ? $old->getRead() : []) + ]; + + if ($shouldUpdate && !$updateValidator->isValid($updatePermissions)) { + throw new AuthorizationException($updateValidator->getDescription()); + } elseif (!$shouldUpdate && !$readValidator->isValid($readPermissions)) { + throw new AuthorizationException($readValidator->getDescription()); } } - $updatePermissions = [ - ...$collection->getUpdate(), - ...($documentSecurity ? $old->getUpdate() : []) - ]; - - $readPermissions = [ - ...$collection->getRead(), - ...($documentSecurity ? $old->getRead() : []) - ]; - - if ($shouldUpdate && !$updateValidator->isValid($updatePermissions)) { - throw new AuthorizationException($updateValidator->getDescription()); - } elseif (!$shouldUpdate && !$readValidator->isValid($readPermissions)) { - throw new AuthorizationException($readValidator->getDescription()); + if ($old->isEmpty()) { + return new Document(); } - } - if ($old->isEmpty()) { - return new Document(); - } + if ($shouldUpdate) { + $updatedAt = $document->getUpdatedAt(); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + } - if ($shouldUpdate) { - $updatedAt = $document->getUpdatedAt(); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - } + // Check if document was updated after the request timestamp + $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); + if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } - // Check if document was updated after the request timestamp - $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); - if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { - throw new ConflictException('Document was updated after the request timestamp'); - } + $document = $this->encode($collection, $document); - $document = $this->encode($collection, $document); + $structureValidator = new Structure($collection); - $structureValidator = new Structure($collection); + if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) + throw new StructureException($structureValidator->getDescription()); + } - if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) - throw new StructureException($structureValidator->getDescription()); - } + if ($this->resolveRelationships) { + $document = $this->silent(fn () => $this->updateDocumentRelationships($collection, $old, $document)); + } - if ($this->resolveRelationships) { - $document = $this->silent(fn () => $this->updateDocumentRelationships($collection, $old, $document)); - } + $this->adapter->updateDocument($collection->getId(), $document); - $this->adapter->updateDocument($collection->getId(), $document); + return $document; + }); if ($this->resolveRelationships) { $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); @@ -3726,9 +3790,7 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->decode($collection, $document); $this->purgeRelatedDocuments($collection, $id); - $this->purgeCachedDocument($collection->getId(), $id); - $this->trigger(self::EVENT_DOCUMENT_UPDATE, $document); return $document; @@ -3753,44 +3815,48 @@ public function updateDocuments(string $collection, array $documents, int $batch return []; } - $time = DateTime::now(); $collection = $this->silent(fn () => $this->getCollection($collection)); - foreach ($documents as $key => $document) { - if (!$document->getId()) { - throw new DatabaseException('Must define $id attribute for each document'); - } + $documents = $this->withTransaction(function () use ($collection, $documents, $batchSize) { + $time = DateTime::now(); - $updatedAt = $document->getUpdatedAt(); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - $document = $this->encode($collection, $document); + foreach ($documents as $key => $document) { + if (!$document->getId()) { + throw new DatabaseException('Must define $id attribute for each document'); + } - $old = Authorization::skip(fn () => $this->silent( - fn () => $this->getDocument( - $collection->getId(), - $document->getId() - ) - )); + $updatedAt = $document->getUpdatedAt(); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + $document = $this->encode($collection, $document); - $validator = new Authorization(self::PERMISSION_UPDATE); - if ( - $collection->getId() !== self::METADATA - && !$validator->isValid($old->getUpdate()) - ) { - throw new AuthorizationException($validator->getDescription()); - } + $old = Authorization::skip(fn () => $this->silent( + fn () => $this->getDocument( + $collection->getId(), + $document->getId(), + forUpdate: true + ) + )); - $validator = new Structure($collection); - if (!$validator->isValid($document)) { - throw new StructureException($validator->getDescription()); - } + $validator = new Authorization(self::PERMISSION_UPDATE); + if ( + $collection->getId() !== self::METADATA + && !$validator->isValid($old->getUpdate()) + ) { + throw new AuthorizationException($validator->getDescription()); + } - if ($this->resolveRelationships) { - $documents[$key] = $this->silent(fn () => $this->updateDocumentRelationships($collection, $old, $document)); + $validator = new Structure($collection); + if (!$validator->isValid($document)) { + throw new StructureException($validator->getDescription()); + } + + if ($this->resolveRelationships) { + $documents[$key] = $this->silent(fn () => $this->updateDocumentRelationships($collection, $old, $document)); + } } - } - $documents = $this->adapter->updateDocuments($collection->getId(), $documents, $batchSize); + return $this->adapter->updateDocuments($collection->getId(), $documents, $batchSize); + }); foreach ($documents as $key => $document) { if ($this->resolveRelationships) { @@ -4394,34 +4460,41 @@ public function deleteDocument(string $collection, string $id): bool $collection = $this->silent(fn () => $this->getCollection($collection)); - $validator = new Authorization(self::PERMISSION_DELETE); + $deleted = $this->withTransaction(function () use ($collection, $id, &$document) { + $document = Authorization::skip(fn () => $this->silent( + fn () => + $this->getDocument($collection->getId(), $id, forUpdate: true) + )); - if ($collection->getId() !== self::METADATA) { - $documentSecurity = $collection->getAttribute('documentSecurity', false); - if (!$validator->isValid([ - ...$collection->getDelete(), - ...($documentSecurity ? $document->getDelete() : []) - ])) { - throw new AuthorizationException($validator->getDescription()); + $validator = new Authorization(self::PERMISSION_DELETE); + + if ($collection->getId() !== self::METADATA) { + $documentSecurity = $collection->getAttribute('documentSecurity', false); + if (!$validator->isValid([ + ...$collection->getDelete(), + ...($documentSecurity ? $document->getDelete() : []) + ])) { + throw new AuthorizationException($validator->getDescription()); + } } - } - // Check if document was updated after the request timestamp - try { - $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); - } catch (Exception $e) { - throw new DatabaseException($e->getMessage(), $e->getCode(), $e); - } + // Check if document was updated after the request timestamp + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } - if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { - throw new ConflictException('Document was updated after the request timestamp'); - } + if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } - if ($this->resolveRelationships) { - $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); - } + if ($this->resolveRelationships) { + $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); + } - $deleted = $this->adapter->deleteDocument($collection->getId(), $id); + return $this->adapter->deleteDocument($collection->getId(), $id); + }); $this->purgeRelatedDocuments($collection, $id); $this->purgeCachedDocument($collection->getId(), $id); From 6eba4de1d4f10e83792f2eb2d3c730ae1c9cb2c5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Aug 2024 16:32:56 +1200 Subject: [PATCH 066/256] Merge pull request #442 from utopia-php/feat-resizing-string-attributes Implement resizing string attributes # Conflicts: # src/Database/Adapter/MariaDB.php # src/Database/Adapter/MySQL.php # src/Database/Adapter/Postgres.php # src/Database/Adapter/SQLite.php --- composer.lock | 34 +++++++-------- docker-compose.yml | 1 + src/Database/Adapter.php | 7 ++++ src/Database/Adapter/MariaDB.php | 26 +++++++----- src/Database/Adapter/Mongo.php | 5 +++ src/Database/Adapter/MySQL.php | 14 ++++--- src/Database/Adapter/Postgres.php | 25 ++++++----- src/Database/Adapter/SQL.php | 10 +++++ src/Database/Adapter/SQLite.php | 14 +++++-- tests/e2e/Adapter/Base.php | 70 ++++++++++++++++++++++++++++++- 10 files changed, 158 insertions(+), 48 deletions(-) diff --git a/composer.lock b/composer.lock index 5bd08c299..b98610e74 100644 --- a/composer.lock +++ b/composer.lock @@ -898,35 +898,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -935,7 +935,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -964,7 +964,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -972,7 +972,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", diff --git a/docker-compose.yml b/docker-compose.yml index e784d8092..82076ebfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,7 @@ services: mariadb: image: mariadb:10.11 container_name: utopia-mariadb + command: mariadbd --max_allowed_packet=1G networks: - database ports: diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 05d651c8d..4295d42d5 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -738,6 +738,13 @@ abstract public function getSupportForRelationships(): bool; abstract public function getSupportForUpdateLock(): bool; + /** + * Is attribute resizing supported? + * + * @return bool + */ + abstract public function getSupportForAttributeResizing(): bool; + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index b7ba2ff6c..e3dd72a09 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -336,10 +336,10 @@ public function updateAttribute(string $collection, string $id, string $type, in { $name = $this->filter($collection); $id = $this->filter($id); + $newKey = empty($newKey) ? null : $this->filter($newKey); $type = $this->getSQLType($type, $size, $signed, $array); if (!empty($newKey)) { - $newKey = $this->filter($newKey); $sql = "ALTER TABLE {$this->getSQLTable($name)} CHANGE COLUMN `{$id}` {$newKey} {$type};"; } else { $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; @@ -347,9 +347,13 @@ public function updateAttribute(string $collection, string $id, string $type, in $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - return $this->getPDO() + try { + return $this->getPDO() ->prepare($sql) ->execute(); + } catch (PDOException $e) { + throw $this->processException($e); + } } /** @@ -2241,28 +2245,28 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL protected function processException(PDOException $e): \Exception { - /** - * PDO and Swoole PDOProxy swap error codes and errorInfo - */ - // Timeout if ($e->getCode() === '70100' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1969) { return new TimeoutException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1969 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '70100') { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); } // Duplicate table if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); } // Duplicate column if ($e->getCode() === '42S21' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1060) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1060 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S21') { + } + + // Data is too big for column resize + if ($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) { + return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); + } + + // Duplicate index + if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1061) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 9e48c9a41..90d3ad310 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1661,6 +1661,11 @@ public function getSupportForUpdateLock(): bool return false; } + public function getSupportForAttributeResizing(): bool + { + return false; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 67be8477f..3503757c4 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -88,24 +88,28 @@ protected function processException(PDOException $e): \Exception // Timeout if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { return new TimeoutException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); } // Duplicate table if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1050 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S01') { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); } // Duplicate column if ($e->getCode() === '42S21' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1060) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1060 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S21') { + } + + // Duplicate index + if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1061) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); } + // Data is too big for column resize + if ($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) { + return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); + } + return $e; } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index edbb00203..6f145b9b7 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -281,6 +281,7 @@ public function deleteCollection(string $id): bool * @param bool $array * * @return bool + * @throws Exception */ public function createAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false): bool { @@ -312,6 +313,7 @@ public function createAttribute(string $collection, string $id, string $type, in * @param bool $array * * @return bool + * @throws DatabaseException */ public function deleteAttribute(string $collection, string $id, bool $array = false): bool { @@ -410,11 +412,15 @@ public function updateAttribute(string $collection, string $id, string $type, in $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - $result = $this->getPDO() + try { + $result = $this->getPDO() ->prepare($sql) ->execute(); - return $result; + return $result; + } catch (PDOException $e) { + throw $this->processException($e); + } } /** @@ -2228,8 +2234,8 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL "; }); } - - /** + + /** * @return string */ public function getLikeOperator(): string @@ -2242,22 +2248,21 @@ protected function processException(PDOException $e): \Exception // Timeout if ($e->getCode() === '57014' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { return new TimeoutException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 7 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '57014') { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); } // Duplicate table if ($e->getCode() === '42P07' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 7 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42P07') { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); } // Duplicate column if ($e->getCode() === '42701' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 7 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42701') { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + } + + // Data is too big for column resize + if ($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { + return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); } return $e; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index e115afdb3..7b6967197 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -351,6 +351,16 @@ public function getSupportForUpdateLock(): bool return true; } + /** + * Is Attribute Resizing Supported? + * + * @return bool + */ + public function getSupportForAttributeResizing(): bool + { + return true; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index e9f4de36e..5ed5f9318 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1095,6 +1095,16 @@ public function getSupportForUpdateLock(): bool return false; } + /** + * Is attribute resizing supported? + * + * @return bool + */ + public function getSupportForAttributeResizing(): bool + { + return false; + } + /** * Get SQL Index Type * @@ -1366,15 +1376,11 @@ protected function processException(PDOException $e): \Exception // Timeout if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { return new TimeoutException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 3024 && isset($e->errorInfo[0]) && $e->errorInfo[0] === "HY000") { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); } // Duplicate if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1) { return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1 && isset($e->errorInfo[0]) && $e->errorInfo[0] === 'HY000') { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); } return $e; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 9d97ec0bf..b462e4c4f 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -29,6 +29,8 @@ use Utopia\Database\Validator\Structure; use Utopia\Validator\Range; +ini_set('memory_limit', '2048M'); + abstract class Base extends TestCase { protected static string $namespace; @@ -5863,6 +5865,72 @@ public function testUpdateAttributeRename(): void } } + public function createRandomString(int $length = 10): string + { + return \substr(\bin2hex(\random_bytes(\max(1, \intval(($length + 1) / 2)))), 0, $length); + } + + public function updateStringAttributeSize(int $size, Document $document): Document + { + static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, $size, true); + + $document = $document->setAttribute('resize_me', $this->createRandomString($size)); + + static::getDatabase()->updateDocument('resize_test', $document->getId(), $document); + $checkDoc = static::getDatabase()->getDocument('resize_test', $document->getId()); + + $this->assertEquals($document->getAttribute('resize_me'), $checkDoc->getAttribute('resize_me')); + $this->assertEquals($size, strlen($checkDoc->getAttribute('resize_me'))); + + return $checkDoc; + } + + public function testUpdateAttributeSize(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForAttributeResizing()) { + $this->expectNotToPerformAssertions(); + return; + } + + static::getDatabase()->createCollection('resize_test'); + + $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true)); + $document = static::getDatabase()->createDocument('resize_test', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resize_me' => $this->createRandomString(128) + ])); + + // Go up in size + + // 0-16381 to 16382-65535 + $document = $this->updateStringAttributeSize(16382, $document); + + // 16382-65535 to 65536-16777215 + $document = $this->updateStringAttributeSize(65536, $document); + + // 65536-16777216 to PHP_INT_MAX or adapter limit + $maxStringSize = 16777217; + $document = $this->updateStringAttributeSize($maxStringSize, $document); + + // Test going down in size with data that is too big (Expect Failure) + try { + static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true); + $this->fail('Succeeded updating attribute size to smaller size with data that is too big'); + } catch (DatabaseException $e) { + $this->assertEquals('Resize would result in data truncation', $e->getMessage()); + } + + // Test going down in size when data isn't too big. + static::getDatabase()->updateDocument('resize_test', $document->getId(), $document->setAttribute('resize_me', $this->createRandomString(128))); + static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true); + } + /** * @depends testCreatedAtUpdatedAt */ @@ -6060,7 +6128,7 @@ public function testKeywords(): void $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); $this->assertEquals('Reserved:' . $keyword, $documents[0]->getAttribute($keyword)); - $documents = $database->find($collectionName, [Query::equal($keyword, ["Reserved:${keyword}"])]); + $documents = $database->find($collectionName, [Query::equal($keyword, ["Reserved:{$keyword}"])]); $this->assertCount(1, $documents); $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); From 46fa933a0b69197950d0ff6705ff8f33c7f68983 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Aug 2024 21:02:45 +1200 Subject: [PATCH 067/256] Merge pull request #444 from utopia-php/feat-resizing-string-attributes Fix VARCHAR -> VARCHAR truncation and add tests # Conflicts: # src/Database/Adapter/MariaDB.php # src/Database/Adapter/MySQL.php --- src/Database/Adapter/MariaDB.php | 3 ++- src/Database/Adapter/MySQL.php | 3 ++- tests/e2e/Adapter/Base.php | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e3dd72a09..bc394ffbd 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2261,7 +2261,8 @@ protected function processException(PDOException $e): \Exception } // Data is too big for column resize - if ($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) { + if (($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) || + ($e->getCode() === '01000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1265)) { return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); } diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 3503757c4..e2b7cc81d 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -106,7 +106,8 @@ protected function processException(PDOException $e): \Exception } // Data is too big for column resize - if ($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) { + if (($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) || + ($e->getCode() === '01000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1265)) { return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b462e4c4f..4248ea903 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -5929,6 +5929,17 @@ public function testUpdateAttributeSize(): void // Test going down in size when data isn't too big. static::getDatabase()->updateDocument('resize_test', $document->getId(), $document->setAttribute('resize_me', $this->createRandomString(128))); static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true); + + // VARCHAR -> VARCHAR Truncation Test + static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 1000, true); + static::getDatabase()->updateDocument('resize_test', $document->getId(), $document->setAttribute('resize_me', $this->createRandomString(1000))); + + try { + static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true); + $this->fail('Succeeded updating attribute size to smaller size with data that is too big'); + } catch (DatabaseException $e) { + $this->assertEquals('Resize would result in data truncation', $e->getMessage()); + } } /** From 6e2547aac0160b7937bedea522cbbd3a3bc23fd8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Aug 2024 22:14:51 +1200 Subject: [PATCH 068/256] Merge pull request #445 from utopia-php/feat-resizing-string-attributes Add Truncate Error class for resizing string attributes # Conflicts: # src/Database/Adapter/MariaDB.php # src/Database/Adapter/MySQL.php # src/Database/Adapter/Postgres.php --- src/Database/Adapter/MariaDB.php | 3 ++- src/Database/Adapter/MySQL.php | 3 ++- src/Database/Adapter/Postgres.php | 1 + src/Database/Exception/Truncate.php | 9 +++++++++ tests/e2e/Adapter/Base.php | 7 +++---- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/Database/Exception/Truncate.php diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index bc394ffbd..d0b2c06b8 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -10,6 +10,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; @@ -2263,7 +2264,7 @@ protected function processException(PDOException $e): \Exception // Data is too big for column resize if (($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) || ($e->getCode() === '01000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1265)) { - return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); + return new TruncateException('Resize would result in data truncation', $e->getCode(), $e); } // Duplicate index diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index e2b7cc81d..9ddc683c3 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -7,6 +7,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Truncate as TruncateException; class MySQL extends MariaDB { @@ -108,7 +109,7 @@ protected function processException(PDOException $e): \Exception // Data is too big for column resize if (($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) || ($e->getCode() === '01000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1265)) { - return new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); + return new TruncateException('Resize would result in data truncation', $e->getCode(), $e); } return $e; diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 6f145b9b7..fcee00ec5 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -11,6 +11,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; diff --git a/src/Database/Exception/Truncate.php b/src/Database/Exception/Truncate.php new file mode 100644 index 000000000..9bd0ffb12 --- /dev/null +++ b/src/Database/Exception/Truncate.php @@ -0,0 +1,9 @@ +updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true); $this->fail('Succeeded updating attribute size to smaller size with data that is too big'); - } catch (DatabaseException $e) { - $this->assertEquals('Resize would result in data truncation', $e->getMessage()); + } catch (TruncateException $e) { } // Test going down in size when data isn't too big. @@ -5937,8 +5937,7 @@ public function testUpdateAttributeSize(): void try { static::getDatabase()->updateAttribute('resize_test', 'resize_me', Database::VAR_STRING, 128, true); $this->fail('Succeeded updating attribute size to smaller size with data that is too big'); - } catch (DatabaseException $e) { - $this->assertEquals('Resize would result in data truncation', $e->getMessage()); + } catch (TruncateException $e) { } } From 4be46b9528dc9cf92dc52853d830cfe9c210ad48 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 2 Sep 2024 18:28:50 +1200 Subject: [PATCH 069/256] Merge pull request #446 from utopia-php/fix-deadlock Fix deadlock when selecting permissions for update --- src/Database/Adapter/MariaDB.php | 8 +++----- src/Database/Adapter/Postgres.php | 2 -- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d0b2c06b8..5ec3b9930 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1074,8 +1074,6 @@ public function updateDocument(string $collection, Document $document): Document $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } - $sql .= ' FOR UPDATE'; - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); /** @@ -1231,9 +1229,9 @@ public function updateDocument(string $collection, Document $document): Document } $sql = " - UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_uid - WHERE _uid = :_uid + UPDATE {$this->getSQLTable($name)} + SET {$columns} _uid = :_uid + WHERE _uid = :_uid "; if ($this->sharedTables) { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index fcee00ec5..3b338137e 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1039,8 +1039,6 @@ public function updateDocument(string $collection, Document $document): Document $sql .= ' AND (_tenant = :_tenant OR _tenant IS NULL)'; } - $sql .= ' FOR UPDATE'; - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); /** From b6b7b54355648d799b3ea1bf6dbdd713bc026eb3 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 10 Sep 2024 22:08:37 +1200 Subject: [PATCH 070/256] Merge pull request #448 from utopia-php/fix-add-escapes-for-rename Fix non-quoted newKey in MariaDB and add newKey filtering for postgres # Conflicts: # composer.lock --- composer.lock | 56 +++++++++++++++++++++++++++---- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Adapter/Postgres.php | 1 + tests/e2e/Adapter/Base.php | 12 +++++++ 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index b98610e74..0f44d6979 100644 --- a/composer.lock +++ b/composer.lock @@ -136,20 +136,20 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -196,7 +196,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -212,7 +212,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "utopia-php/cache", @@ -269,11 +269,53 @@ "version": "0.33.8", "source": { "type": "git", + "url": "https://github.com/utopia-php/di.git", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" "url": "https://github.com/utopia-php/http.git", "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" }, "dist": { "type": "zip", + "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/", + "Tests\\E2E\\": "tests/e2e" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple and lite library for managing dependency injections", + "keywords": [ + "framework", + "http", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/di/issues", + "source": "https://github.com/utopia-php/di/tree/0.1.0" + }, + "time": "2024-08-08T14:35:19+00:00" + }, + { + "name": "utopia-php/framework", "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", "shasum": "" diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 5ec3b9930..d4752bc0c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -341,7 +341,7 @@ public function updateAttribute(string $collection, string $id, string $type, in $type = $this->getSQLType($type, $size, $signed, $array); if (!empty($newKey)) { - $sql = "ALTER TABLE {$this->getSQLTable($name)} CHANGE COLUMN `{$id}` {$newKey} {$type};"; + $sql = "ALTER TABLE {$this->getSQLTable($name)} CHANGE COLUMN `{$id}` `{$newKey}` {$type};"; } else { $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 3b338137e..53187dd04 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -379,6 +379,7 @@ public function updateAttribute(string $collection, string $id, string $type, in { $name = $this->filter($collection); $id = $this->filter($id); + $newKey = empty($newKey) ? null : $this->filter($newKey); $type = $this->getSQLType($type, $size, $signed, $array); if ($type == 'TIMESTAMP(3)') { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 302dfd223..7ae1ba50d 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -5864,6 +5864,18 @@ public function testUpdateAttributeRename(): void } catch (\Exception $e) { $this->assertInstanceOf(StructureException::class, $e); } + + // Check new key filtering + static::getDatabase()->updateAttribute( + collection: 'rename_test', + id: 'renamed', + newKey: 'renamed-test', + ); + + $doc = static::getDatabase()->getDocument('rename_test', $doc->getId()); + + $this->assertEquals('string', $doc->getAttribute('renamed-test')); + $this->assertArrayNotHasKey('renamed', $doc->getAttributes()); } public function createRandomString(int $length = 10): string From 32ab2f79315906053586513a3e8f994cb8f783e5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 12 Sep 2024 23:01:10 +1200 Subject: [PATCH 071/256] Lint --- composer.lock | 58 ++++--------------------- src/Database/Adapter/MariaDB.php | 22 +++++----- src/Database/Adapter/Mongo.php | 7 ++- src/Database/Adapter/Postgres.php | 27 +++++------- src/Database/Adapter/SQL.php | 6 +-- src/Database/Adapter/SQLite.php | 12 ++--- src/Database/Database.php | 2 +- src/Database/Exception.php | 2 +- src/Database/Query.php | 4 +- src/Database/Validator/Datetime.php | 18 ++++---- src/Database/Validator/Index.php | 14 +++--- src/Database/Validator/Queries.php | 4 +- src/Database/Validator/Query/Filter.php | 18 ++++---- src/Database/Validator/Structure.php | 2 +- tests/e2e/Adapter/Base.php | 42 +++++++++--------- tests/unit/Validator/DateTimeTest.php | 2 +- 16 files changed, 97 insertions(+), 143 deletions(-) diff --git a/composer.lock b/composer.lock index 0f44d6979..32455f27d 100644 --- a/composer.lock +++ b/composer.lock @@ -269,53 +269,11 @@ "version": "0.33.8", "source": { "type": "git", - "url": "https://github.com/utopia-php/di.git", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" "url": "https://github.com/utopia-php/http.git", "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "laravel/pint": "^1.2", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25", - "swoole/ide-helper": "4.8.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\": "src/", - "Tests\\E2E\\": "tests/e2e" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A simple and lite library for managing dependency injections", - "keywords": [ - "framework", - "http", - "php", - "upf" - ], - "support": { - "issues": "https://github.com/utopia-php/di/issues", - "source": "https://github.com/utopia-php/di/tree/0.1.0" - }, - "time": "2024-08-08T14:35:19+00:00" - }, - { - "name": "utopia-php/framework", "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", "shasum": "" @@ -548,16 +506,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.2", + "version": "v1.17.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" + "reference": "9d77be916e145864f10788bb94531d03e1f7b482" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", + "reference": "9d77be916e145864f10788bb94531d03e1f7b482", "shasum": "" }, "require": { @@ -568,13 +526,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.61.1", - "illuminate/view": "^10.48.18", + "friendsofphp/php-cs-fixer": "^3.64.0", + "illuminate/view": "^10.48.20", "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.0" + "pestphp/pest": "^2.35.1" }, "bin": [ "builds/pint" @@ -610,7 +568,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-06T15:11:54+00:00" + "time": "2024-09-03T15:00:28+00:00" }, { "name": "myclabs/deep-copy", diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d4752bc0c..66f165550 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -705,7 +705,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; - if(!empty($collectionAttribute['array']) && $this->castIndexArray()) { + if (!empty($collectionAttribute['array']) && $this->castIndexArray()) { $attributes[$i] = '(CAST(' . $attr . ' AS char(' . Database::ARRAY_INDEX_LENGTH . ') ARRAY))'; } } @@ -883,7 +883,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->execute(); } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -933,7 +933,7 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); - if(!empty($document->getInternalId())) { + if (!empty($document->getInternalId())) { $internalIds[$document->getId()] = true; $attributes['_id'] = $document->getInternalId(); } @@ -1014,7 +1014,7 @@ public function createDocuments(string $collection, array $documents, int $batch } } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1026,7 +1026,7 @@ public function createDocuments(string $collection, array $documents, int $batch } foreach ($documents as $document) { - if(!isset($internalIds[$document->getId()])) { + if (!isset($internalIds[$document->getId()])) { $document['$internalId'] = $this->getDocument( $collection, $document->getId(), @@ -1270,7 +1270,7 @@ public function updateDocument(string $collection, Document $document): Document } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1527,7 +1527,7 @@ public function updateDocuments(string $collection, array $documents, int $batch } } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1757,7 +1757,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -1883,7 +1883,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $limit = \is_null($max) ? '' : 'LIMIT :max'; $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -2098,7 +2098,7 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - if($this->getSupportForJSONOverlaps() && $query->onArray()) { + if ($this->getSupportForJSONOverlaps() && $query->onArray()) { return "JSON_OVERLAPS(`table_main`.{$attribute}, :{$placeholder}_0)"; } @@ -2124,7 +2124,7 @@ protected function getSQLCondition(Query $query): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if($array === true) { + if ($array === true) { return 'JSON'; } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 90d3ad310..c4e20110f 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -3,7 +3,6 @@ namespace Utopia\Database\Adapter; use Exception; - use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; @@ -291,7 +290,7 @@ public function getSizeOfCollection(string $collection): int } else { throw new DatabaseException('No size found'); } - } catch(Exception $e) { + } catch (Exception $e) { throw new DatabaseException('Failed to get collection size: ' . $e->getMessage()); } } @@ -1364,7 +1363,7 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr $queries = Query::groupByType($queries)['filters']; foreach ($queries as $query) { /* @var $query Query */ - if($query->isNested()) { + if ($query->isNested()) { $operator = $this->getQueryOperator($query->getMethod()); $filters[$separator][] = $this->buildFilters($query->getValues(), $operator); } else { @@ -1418,7 +1417,7 @@ protected function buildFilter(Query $query): array } elseif ($operator == '$ne' && \is_array($value)) { $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { - if($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { + if ($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { $filter[$attribute]['$regex'] = new Regex(".*{$this->escapeWildcards($value)}.*", 'i'); } else { $filter[$attribute]['$in'] = $query->getValues(); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 53187dd04..521e58d40 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -11,7 +11,6 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; -use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; @@ -864,7 +863,7 @@ public function createDocument(string $collection, Document $document): Document $queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions); $stmtPermissions = $this->getPDO()->prepare($queryPermissions); - if($sqlTenant) { + if ($sqlTenant) { $stmtPermissions->bindValue(':_tenant', $this->tenant); } } @@ -880,7 +879,7 @@ public function createDocument(string $collection, Document $document): Document } catch (Throwable $e) { switch ($e->getCode()) { case 23505: - throw new Duplicate('Duplicated document: ' . $e->getMessage()); + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); default: throw $e; } @@ -924,12 +923,12 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); - if(!empty($document->getInternalId())) { + if (!empty($document->getInternalId())) { $internalIds[$document->getId()] = true; $attributes['_id'] = $document->getInternalId(); } - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -984,8 +983,6 @@ public function createDocuments(string $collection, array $documents, int $batch $stmtPermissions?->execute(); } } - - return $documents; } catch (PDOException $e) { throw match ($e->getCode()) { 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), @@ -994,7 +991,7 @@ public function createDocuments(string $collection, array $documents, int $batch } foreach ($documents as $document) { - if(!isset($internalIds[$document->getId()])) { + if (!isset($internalIds[$document->getId()])) { $document['$internalId'] = $this->getDocument( $collection, $document->getId(), @@ -1271,7 +1268,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -1463,7 +1460,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); } - if($this->sharedTables) { + if ($this->sharedTables) { $stmtAddPermissions->bindValue(':_tenant', $this->tenant); } @@ -1697,7 +1694,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -1823,7 +1820,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $limit = \is_null($max) ? '' : 'LIMIT :max'; $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -2061,7 +2058,7 @@ protected function getFulltextValue(string $value): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if($array === true) { + if ($array === true) { return 'JSONB'; } @@ -2234,8 +2231,8 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL "; }); } - - /** + + /** * @return string */ public function getLikeOperator(): string diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 7b6967197..d7bce4f3a 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -818,14 +818,14 @@ protected function bindConditionValue(mixed $stmt, Query $query): void return; } - if($query->isNested()) { + if ($query->isNested()) { foreach ($query->getValues() as $value) { $this->bindConditionValue($stmt, $value); } return; } - if($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { + if ($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { $placeholder = $this->getSQLPlaceholder($query) . '_0'; $stmt->bindValue($placeholder, json_encode($query->getValues()), PDO::PARAM_STR); return; @@ -1071,7 +1071,7 @@ public function getSQLConditions(array $queries = [], string $separator = 'AND') continue; } - if($query->isNested()) { + if ($query->isNested()) { $conditions[] = $this->getSQLConditions($query->getValues(), $query->getMethod()); } else { $conditions[] = $this->getSQLCondition($query); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5ed5f9318..08c3baa7b 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -459,7 +459,7 @@ public function createDocument(string $collection, Document $document): Document $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -533,7 +533,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions = $this->getPDO()->prepare($queryPermissions); - if($this->sharedTables) { + if ($this->sharedTables) { $stmtPermissions->bindValue(':_tenant', $this->tenant); } } @@ -578,7 +578,7 @@ public function updateDocument(string $collection, Document $document): Document $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -712,7 +712,7 @@ public function updateDocument(string $collection, Document $document): Document $stmtAddPermissions = $this->getPDO()->prepare($sql); $stmtAddPermissions->bindValue(":_uid", $document->getId()); - if($this->sharedTables) { + if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } @@ -825,7 +825,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -1023,7 +1023,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); } - if($this->sharedTables) { + if ($this->sharedTables) { $stmtAddPermissions->bindValue(':_tenant', $this->tenant); } diff --git a/src/Database/Database.php b/src/Database/Database.php index 61b2f2b98..5421cd895 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5048,7 +5048,7 @@ public function find(string $collection, array $queries = []): array $results = $skipAuth ? Authorization::skip($getResults) : $getResults(); - foreach ($results as &$node) { + foreach ($results as &$node) { if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) { $node = $this->silent(fn () => $this->populateDocumentRelationships($collection, $node, $nestedSelections)); } diff --git a/src/Database/Exception.php b/src/Database/Exception.php index 64ad3c997..94099c6ae 100644 --- a/src/Database/Exception.php +++ b/src/Database/Exception.php @@ -8,7 +8,7 @@ class Exception extends \Exception { public function __construct(string $message, int|string $code = 0, Throwable $previous = null) { - if(\is_string($code)) { + if (\is_string($code)) { if (\is_numeric($code)) { $code = (int) $code; } else { diff --git a/src/Database/Query.php b/src/Database/Query.php index b01534aab..8e9e574d5 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -288,7 +288,7 @@ public function toArray(): array { $array = ['method' => $this->method]; - if(!empty($this->attribute)) { + if (!empty($this->attribute)) { $array['attribute'] = $this->attribute; } @@ -681,7 +681,7 @@ public static function groupByType(array $queries): array */ public function isNested(): bool { - if(in_array($this->getMethod(), self::LOGICAL_TYPES)) { + if (in_array($this->getMethod(), self::LOGICAL_TYPES)) { return true; } diff --git a/src/Database/Validator/Datetime.php b/src/Database/Validator/Datetime.php index e8ffee5ff..812669d7a 100644 --- a/src/Database/Validator/Datetime.php +++ b/src/Database/Validator/Datetime.php @@ -33,7 +33,7 @@ class Datetime extends Validator */ public function __construct(bool $requireDateInFuture = false, string $precision = self::PRECISION_ANY, int $offset = 0) { - if($offset < 0) { + if ($offset < 0) { throw new \Exception('Offset must be a positive number.'); } @@ -50,13 +50,13 @@ public function getDescription(): string { $message = 'Value must be valid date'; - if($this->offset > 0) { + if ($this->offset > 0) { $message .= " at least " . $this->offset . " seconds in future"; - } elseif($this->requireDateInFuture) { + } elseif ($this->requireDateInFuture) { $message .= " in future"; } - if($this->precision !== self::PRECISION_ANY) { + if ($this->precision !== self::PRECISION_ANY) { $message .= " with " . $this->precision . " precision"; } @@ -84,9 +84,9 @@ public function isValid($value): bool return false; } - if($this->offset !== 0) { + if ($this->offset !== 0) { $diff = $date->getTimestamp() - $now->getTimestamp(); - if($diff <= $this->offset) { + if ($diff <= $this->offset) { return false; } } @@ -109,12 +109,12 @@ public function isValid($value): bool break; } - foreach($denyConstants as $constant) { - if(\intval($date->format($constant)) !== 0) { + foreach ($denyConstants as $constant) { + if (\intval($date->format($constant)) !== 0) { return false; } } - } catch(\Exception $e) { + } catch (\Exception $e) { return false; } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index b24e39f73..fdaa1efe0 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -126,30 +126,30 @@ public function checkArrayIndex(Document $index): bool foreach ($attributes as $attributePosition => $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); - if($attribute->getAttribute('array', false)) { + if ($attribute->getAttribute('array', false)) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values - if($index->getAttribute('type') != Database::INDEX_KEY) { + if ($index->getAttribute('type') != Database::INDEX_KEY) { $this->message = '"' . ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } - if(empty($lengths[$attributePosition])) { + if (empty($lengths[$attributePosition])) { $this->message = 'Index length for array not specified'; return false; } $arrayAttributes[] = $attribute->getAttribute('key', ''); - if(count($arrayAttributes) > 1) { + if (count($arrayAttributes) > 1) { $this->message = 'An index may only contain one array attribute'; return false; } $direction = $orders[$attributePosition] ?? ''; - if(!empty($direction)) { + if (!empty($direction)) { $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; return false; } - } elseif($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + } elseif ($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; return false; } @@ -188,7 +188,7 @@ public function checkIndexLength(Document $index): bool break; } - if($attribute->getAttribute('array', false)) { + if ($attribute->getAttribute('array', false)) { $attributeSize = Database::ARRAY_INDEX_LENGTH; $indexLength = Database::ARRAY_INDEX_LENGTH; } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 2e4aac71a..b1d67aad0 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -71,8 +71,8 @@ public function isValid($value): bool } } - if($query->isNested()) { - if(!self::isValid($query->getValues())) { + if ($query->isNested()) { + if (!self::isValid($query->getValues())) { return false; } } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index f5027e6c5..4767d545c 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -132,29 +132,29 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } } - if($attributeSchema['type'] === 'relationship') { + if ($attributeSchema['type'] === 'relationship') { /** * We can not disable relationship query since we have logic that use it, * so instead we validate against the relation type */ $options = $attributeSchema['options']; - if($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { + if ($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + if ($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + if ($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + if ($options['relationType'] === Database::RELATION_MANY_TO_MANY) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } @@ -162,7 +162,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $array = $attributeSchema['array'] ?? false; - if( + if ( !$array && $method === Query::TYPE_CONTAINS && $attributeSchema['type'] !== Database::VAR_STRING @@ -171,7 +171,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s return false; } - if( + if ( $array && !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) ) { @@ -254,12 +254,12 @@ public function isValid($value): bool case Query::TYPE_AND: $filters = Query::groupByType($value->getValues())['filters']; - if(count($value->getValues()) !== count($filters)) { + if (count($value->getValues()) !== count($filters)) { $this->message = \ucfirst($method) . ' queries can only contain filter queries'; return false; } - if(count($filters) < 2) { + if (count($filters) < 2) { $this->message = \ucfirst($method) . ' queries require at least two queries'; return false; } diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 12b753824..991a32240 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -257,7 +257,7 @@ public function isValid($document): bool continue; } - if($type === Database::VAR_RELATIONSHIP) { + if ($type === Database::VAR_RELATIONSHIP) { continue; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 7ae1ba50d..7d6852089 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1028,7 +1028,7 @@ public function testQueryTimeout(): void Query::notEqual('longtext', 'appwrite'), ]); $this->fail('Failed to throw exception'); - } catch(\Exception $e) { + } catch (\Exception $e) { static::getDatabase()->clearTimeout(); static::getDatabase()->deleteCollection('global-timeouts'); $this->assertInstanceOf(TimeoutException::class, $e); @@ -1869,7 +1869,7 @@ public function testCreateDocument(): Document 'empty' => [], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertTrue($e instanceof StructureException); $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); } @@ -1888,7 +1888,7 @@ public function testCreateDocument(): Document 'empty' => [], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertTrue($e instanceof StructureException); $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); } @@ -2279,7 +2279,7 @@ public function testListDocumentSearch(): void public function testEmptyTenant(): void { - if(static::getDatabase()->getAdapter()->getSharedTables()) { + if (static::getDatabase()->getAdapter()->getSharedTables()) { $this->expectNotToPerformAssertions(); return; } @@ -2458,7 +2458,7 @@ public function testUpdateDocumentConflict(Document $document): void return $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); }); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertTrue($e instanceof ConflictException); $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); } @@ -2599,7 +2599,7 @@ public function testArrayAttribute(): void try { $database->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); } @@ -2619,7 +2619,7 @@ public function testArrayAttribute(): void 'short' => ['More than 5 size'], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "short[\'0\']" has invalid type. Value must be a valid string and no longer than 5 chars', $e->getMessage()); } @@ -2628,7 +2628,7 @@ public function testArrayAttribute(): void 'names' => ['Joe', 100], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "names[\'1\']" has invalid type. Value must be a valid string and no longer than 255 chars', $e->getMessage()); } @@ -2637,7 +2637,7 @@ public function testArrayAttribute(): void 'age' => 1.5, ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); } @@ -2646,7 +2646,7 @@ public function testArrayAttribute(): void 'age' => -100, ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); } @@ -2675,7 +2675,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); } else { @@ -2686,7 +2686,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); } @@ -2707,7 +2707,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); } } @@ -2718,7 +2718,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Cannot set a length on "integer" attributes', $e->getMessage()); } @@ -2731,7 +2731,7 @@ public function testArrayAttribute(): void Query::equal('names', ['Joe']), ]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid query: Cannot query equal on attribute "names" because it is an array.', $e->getMessage()); } @@ -2740,7 +2740,7 @@ public function testArrayAttribute(): void Query::contains('age', [10]) ]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array or string.', $e->getMessage()); } @@ -3176,7 +3176,7 @@ public function testFindContains(): void Query::contains('price', [10.5]), ]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid query: Cannot query contains on attribute "price" because it is not an array or string.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } @@ -4083,7 +4083,7 @@ public function testOrSingleQuery(): void ]) ]); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertEquals('Invalid query: Or queries require at least two queries', $e->getMessage()); } } @@ -4143,7 +4143,7 @@ public function testAndSingleQuery(): void ]) ]); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertEquals('Invalid query: And queries require at least two queries', $e->getMessage()); } } @@ -4930,7 +4930,7 @@ public function testStructureValidationAfterRelationsAttribute(): void 'name' => 'Frozen', // Unknown attribute 'name' after relation attribute ])); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertInstanceOf(StructureException::class, $e); } } @@ -5005,7 +5005,7 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void try { static::getDatabase()->updateDocument('level1', $level1->getId(), $level1->setAttribute('name', 'haha')); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertInstanceOf(AuthorizationException::class, $e); } $level1->setAttribute('name', 'Level 1'); diff --git a/tests/unit/Validator/DateTimeTest.php b/tests/unit/Validator/DateTimeTest.php index b9157e8b8..d8ababb6f 100644 --- a/tests/unit/Validator/DateTimeTest.php +++ b/tests/unit/Validator/DateTimeTest.php @@ -128,7 +128,7 @@ public function testOffset(): void $threwException = false; try { $dateValidator = new DatetimeValidator(offset: -60); - } catch(\Exception $e) { + } catch (\Exception $e) { $threwException = true; } $this->assertTrue($threwException); From b3cf76764ccb22a22e6fe680b777f0a145747743 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 13 Sep 2024 23:40:41 +1200 Subject: [PATCH 072/256] Add rollbacks on bad state --- src/Database/Adapter/SQL.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index d7bce4f3a..1e1bfaebc 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -34,6 +34,10 @@ public function startTransaction(): bool { try { if ($this->inTransaction === 0) { + if ($this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); + } + $result = $this->getPDO()->beginTransaction(); } else { $result = true; @@ -66,12 +70,18 @@ public function commitTransaction(): bool try { $result = $this->getPDO()->commit(); } catch (PDOException $e) { + if ($this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); + } throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { $this->inTransaction--; } if (!$result) { + if ($this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); + } throw new DatabaseException('Failed to commit transaction'); } From 12b634a1c7f5e99dc4a5f6ff26ddcf7872d8c1c3 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 4 Oct 2024 12:30:56 +0900 Subject: [PATCH 073/256] Fix metadata becoming desynchronised when updating relationship names --- src/Database/Database.php | 130 +++++++++++++++++++++---------------- tests/e2e/Adapter/Base.php | 56 ++++++++++++++++ 2 files changed, 130 insertions(+), 56 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 587a9bc49..3579210e6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2250,7 +2250,20 @@ public function updateRelationship( throw new DuplicateException('Attribute already exists'); } - $this->updateAttributeMeta($collection->getId(), $id, function ($attribute) use ($collection, $id, $newKey, $newTwoWayKey, $twoWay, $onDelete) { + $attributeIndex = array_search($id, array_map(fn ($attribute) => $attribute['$id'], $attributes)); + + if ($attributeIndex === false) { + throw new DatabaseException('Attribute not found'); + } + + $attribute = $attributes[$attributeIndex]; + $type = $attribute['options']['relationType']; + $side = $attribute['options']['side']; + + $relatedCollectionId = $attribute['options']['relatedCollection']; + $relatedCollection = $this->getCollection($relatedCollectionId); + + $this->updateAttributeMeta($collection->getId(), $id, function ($attribute) use ($collection, $id, $newKey, $newTwoWayKey, $twoWay, $onDelete, $type, $side) { $altering = (!\is_null($newKey) && $newKey !== $id) || (!\is_null($newTwoWayKey) && $newTwoWayKey !== $attribute['options']['twoWayKey']); @@ -2265,9 +2278,6 @@ public function updateRelationship( throw new DuplicateException('Related attribute already exists'); } - $type = $attribute['options']['relationType']; - $side = $attribute['options']['side']; - $newKey ??= $attribute['key']; $twoWayKey = $attribute['options']['twoWayKey']; $newTwoWayKey ??= $attribute['options']['twoWayKey']; @@ -2329,68 +2339,76 @@ public function updateRelationship( throw new DatabaseException('Failed to update relationship'); } } + }); - $this->purgeCachedCollection($collection->getId()); - $this->purgeCachedCollection($relatedCollection->getId()); + // Update Indexes + $renameIndex = function (string $collection, string $key, string $newKey) { + $this->updateIndexMeta( + $collection, + '_index_' . $key, + function ($index) use ($newKey) { + $index->setAttribute('attributes', [$newKey]); + } + ); + $this->silent( + fn () => + $this->renameIndex($collection, '_index_' . $key, '_index_' . $newKey) + ); + }; - $renameIndex = function (string $collection, string $key, string $newKey) { - $this->updateIndexMeta( - $collection, - '_index_' . $key, - fn ($index) => - $index->setAttribute('attributes', [$newKey]) - ); - $this->silent( - fn () => - $this->renameIndex($collection, '_index_' . $key, '_index_' . $newKey) - ); - }; + $newKey ??= $attribute['key']; + $twoWayKey = $attribute['options']['twoWayKey']; + $newTwoWayKey ??= $attribute['options']['twoWayKey']; + $twoWay ??= $attribute['options']['twoWay']; + $onDelete ??= $attribute['options']['onDelete']; - switch ($type) { - case self::RELATION_ONE_TO_ONE: - if ($id !== $newKey) { - $renameIndex($collection->getId(), $id, $newKey); - } - if ($twoWay && $twoWayKey !== $newTwoWayKey) { + switch ($type) { + case self::RELATION_ONE_TO_ONE: + if ($id !== $newKey) { + $renameIndex($collection->getId(), $id, $newKey); + } + if ($twoWay && $twoWayKey !== $newTwoWayKey) { + $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); + } + break; + case self::RELATION_ONE_TO_MANY: + if ($side === Database::RELATION_SIDE_PARENT) { + if ($twoWayKey !== $newTwoWayKey) { $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); } - break; - case self::RELATION_ONE_TO_MANY: - if ($side === Database::RELATION_SIDE_PARENT) { - if ($twoWayKey !== $newTwoWayKey) { - $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); - } - } else { - if ($id !== $newKey) { - $renameIndex($collection->getId(), $id, $newKey); - } - } - break; - case self::RELATION_MANY_TO_ONE: - if ($side === Database::RELATION_SIDE_PARENT) { - if ($id !== $newKey) { - $renameIndex($collection->getId(), $id, $newKey); - } - } else { - if ($twoWayKey !== $newTwoWayKey) { - $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); - } + } else { + if ($id !== $newKey) { + $renameIndex($collection->getId(), $id, $newKey); } - break; - case self::RELATION_MANY_TO_MANY: - $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); - + } + break; + case self::RELATION_MANY_TO_ONE: + if ($side === Database::RELATION_SIDE_PARENT) { if ($id !== $newKey) { - $renameIndex($junction, $id, $newKey); + $renameIndex($collection->getId(), $id, $newKey); } + } else { if ($twoWayKey !== $newTwoWayKey) { - $renameIndex($junction, $twoWayKey, $newTwoWayKey); + $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); } - break; - default: - throw new RelationshipException('Invalid relationship type.'); - } - }); + } + break; + case self::RELATION_MANY_TO_MANY: + $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); + + if ($id !== $newKey) { + $renameIndex($junction, $id, $newKey); + } + if ($twoWayKey !== $newTwoWayKey) { + $renameIndex($junction, $twoWayKey, $newTwoWayKey); + } + break; + default: + throw new RelationshipException('Invalid relationship type.'); + } + + $this->purgeCachedCollection($collection->getId()); + $this->purgeCachedCollection($relatedCollection->getId()); return true; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 8b1dcec2a..f5e119769 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -5969,6 +5969,62 @@ public function testUpdateAttributeRename(): void $this->assertArrayNotHasKey('renamed', $doc->getAttributes()); } + public function testUpdateAttributeRenameRelationshipTwoWay(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + static::getDatabase()->createCollection('rename_relationship_test_a'); + static::getDatabase()->createCollection('rename_relationship_test_b'); + + static::getDatabase()->createAttribute('rename_relationship_test_b', 'name', Database::VAR_STRING, 255, true); + + static::getDatabase()->createRelationship( + 'rename_relationship_test_a', + 'rename_relationship_test_b', + Database::RELATION_ONE_TO_ONE, + true + ); + + $docA = static::getDatabase()->createDocument('rename_relationship_test_a', new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'rename_relationship_test_b' => [ + '$id' => 'b1', + 'name' => 'B1' + ] + ])); + + $docB = static::getDatabase()->getDocument('rename_relationship_test_b', 'b1'); + $this->assertArrayHasKey('rename_relationship_test_a', $docB->getAttributes()); + $this->assertEquals('B1', $docB->getAttribute('name')); + + // Rename attribute + static::getDatabase()->updateRelationship( + collection: 'rename_relationship_test_a', + id: 'rename_relationship_test_b', + newKey: 'rename_relationship_test_b_renamed' + ); + + // Rename again + static::getDatabase()->updateRelationship( + collection: 'rename_relationship_test_a', + id: 'rename_relationship_test_b_renamed', + newKey: 'rename_relationship_test_b_renamed_2' + ); + + // Check our data is OK + $docA = static::getDatabase()->getDocument('rename_relationship_test_a', $docA->getId()); + $this->assertArrayHasKey('rename_relationship_test_b_renamed_2', $docA->getAttributes()); + $this->assertEquals($docB->getId(), $docA->getAttribute('rename_relationship_test_b_renamed_2')['$id']); + } + public function createRandomString(int $length = 10): string { return \substr(\bin2hex(\random_bytes(\max(1, \intval(($length + 1) / 2)))), 0, $length); From 50b30f82bb8ecf9d5e841baf4986c4335fdcd230 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 4 Oct 2024 13:38:09 +0900 Subject: [PATCH 074/256] Postgres has a max table/index name length of 63 bytes --- tests/e2e/Adapter/Base.php | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index f5e119769..173e0dae9 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -5976,53 +5976,53 @@ public function testUpdateAttributeRenameRelationshipTwoWay(): void return; } - static::getDatabase()->createCollection('rename_relationship_test_a'); - static::getDatabase()->createCollection('rename_relationship_test_b'); + static::getDatabase()->createCollection('rn_rs_test_a'); + static::getDatabase()->createCollection('rn_rs_test_b'); - static::getDatabase()->createAttribute('rename_relationship_test_b', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('rn_rs_test_b', 'name', Database::VAR_STRING, 255, true); static::getDatabase()->createRelationship( - 'rename_relationship_test_a', - 'rename_relationship_test_b', + 'rn_rs_test_a', + 'rn_rs_test_b', Database::RELATION_ONE_TO_ONE, true ); - $docA = static::getDatabase()->createDocument('rename_relationship_test_a', new Document([ + $docA = static::getDatabase()->createDocument('rn_rs_test_a', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], - 'rename_relationship_test_b' => [ + 'rn_rs_test_b' => [ '$id' => 'b1', 'name' => 'B1' ] ])); - $docB = static::getDatabase()->getDocument('rename_relationship_test_b', 'b1'); - $this->assertArrayHasKey('rename_relationship_test_a', $docB->getAttributes()); + $docB = static::getDatabase()->getDocument('rn_rs_test_b', 'b1'); + $this->assertArrayHasKey('rn_rs_test_a', $docB->getAttributes()); $this->assertEquals('B1', $docB->getAttribute('name')); // Rename attribute static::getDatabase()->updateRelationship( - collection: 'rename_relationship_test_a', - id: 'rename_relationship_test_b', - newKey: 'rename_relationship_test_b_renamed' + collection: 'rn_rs_test_a', + id: 'rn_rs_test_b', + newKey: 'rn_rs_test_b_renamed' ); // Rename again static::getDatabase()->updateRelationship( - collection: 'rename_relationship_test_a', - id: 'rename_relationship_test_b_renamed', - newKey: 'rename_relationship_test_b_renamed_2' + collection: 'rn_rs_test_a', + id: 'rn_rs_test_b_renamed', + newKey: 'rn_rs_test_b_renamed_2' ); // Check our data is OK - $docA = static::getDatabase()->getDocument('rename_relationship_test_a', $docA->getId()); - $this->assertArrayHasKey('rename_relationship_test_b_renamed_2', $docA->getAttributes()); - $this->assertEquals($docB->getId(), $docA->getAttribute('rename_relationship_test_b_renamed_2')['$id']); + $docA = static::getDatabase()->getDocument('rn_rs_test_a', $docA->getId()); + $this->assertArrayHasKey('rn_rs_test_b_renamed_2', $docA->getAttributes()); + $this->assertEquals($docB->getId(), $docA->getAttribute('rn_rs_test_b_renamed_2')['$id']); } public function createRandomString(int $length = 10): string From 4fd55874069b0e2b3545de41037a2c0f8404ecc4 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Tue, 17 Sep 2024 18:02:59 +0900 Subject: [PATCH 075/256] Make findOne return empty document instead of false when no matches are found --- src/Database/Database.php | 24 ++++++++++++--------- tests/e2e/Adapter/Base.php | 44 +++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 587a9bc49..008aedc97 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4035,10 +4035,10 @@ private function updateDocumentRelationships(Document $collection, Document $old } if ( $oldValue?->getId() !== $value - && $this->skipRelationships(fn () => $this->findOne($relatedCollection->getId(), [ + && !($this->skipRelationships(fn () => $this->findOne($relatedCollection->getId(), [ Query::select(['$id']), Query::equal($twoWayKey, [$value]), - ])) + ]))->isEmpty()) ) { // Have to do this here because otherwise relations would be updated before the database can throw the unique violation throw new DuplicateException('Document already has a related document'); @@ -4056,10 +4056,10 @@ private function updateDocumentRelationships(Document $collection, Document $old if ( $oldValue?->getId() !== $value->getId() - && $this->skipRelationships(fn () => $this->findOne($relatedCollection->getId(), [ + && !($this->skipRelationships(fn () => $this->findOne($relatedCollection->getId(), [ Query::select(['$id']), Query::equal($twoWayKey, [$value->getId()]), - ])) + ]))->isEmpty()) ) { // Have to do this here because otherwise relations would be updated before the database can throw the unique violation throw new DuplicateException('Document already has a related document'); @@ -4726,7 +4726,7 @@ private function deleteRestrict( Query::equal($twoWayKey, [$document->getId()]) ]); - if (!$related instanceof Document) { + if ($related->isEmpty()) { return; } @@ -4749,7 +4749,7 @@ private function deleteRestrict( Query::equal($twoWayKey, [$document->getId()]) ])); - if ($related) { + if (!$related->isEmpty()) { throw new RestrictedException('Cannot delete document because it has at least one related document.'); } } @@ -4793,7 +4793,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection $related = $this->getDocument($relatedCollection->getId(), $value->getId(), [Query::select(['$id'])]); } - if (!$related instanceof Document) { + if ($related->isEmpty()) { return; } @@ -5172,10 +5172,10 @@ public function find(string $collection, array $queries = []): array /** * @param string $collection * @param array $queries - * @return false|Document + * @return Document * @throws DatabaseException */ - public function findOne(string $collection, array $queries = []): false|Document + public function findOne(string $collection, array $queries = []): Document { $results = $this->silent(fn () => $this->find($collection, \array_merge([ Query::limit(1) @@ -5185,7 +5185,11 @@ public function findOne(string $collection, array $queries = []): false|Document $this->trigger(self::EVENT_DOCUMENT_FIND, $found); - return $found; + if (!$found) { + return new Document(); + } else { + return $found; + } } /** diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 8b1dcec2a..487061511 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -4280,13 +4280,13 @@ public function testFindOne(): void Query::orderAsc('name') ]); - $this->assertTrue($document instanceof Document); + $this->assertFalse($document->isEmpty()); $this->assertEquals('Frozen', $document->getAttribute('name')); $document = static::getDatabase()->findOne('movies', [ Query::offset(10) ]); - $this->assertEquals(false, $document); + $this->assertTrue($document->isEmpty()); } public function testFindNull(): void @@ -5524,7 +5524,7 @@ public function testRenameAttribute(): void // Document should be there if adapter migrated properly $document = $database->findOne('colors'); - $this->assertTrue($document instanceof Document); + $this->assertFalse($document->isEmpty()); $this->assertEquals('black', $document->getAttribute('verbose')); $this->assertEquals('#000000', $document->getAttribute('hex')); $this->assertEquals(null, $document->getAttribute('name')); @@ -6568,7 +6568,7 @@ public function testOneToOneOneWayRelationship(): void Query::select(['*', 'library.name']) ]); - if (!$person instanceof Document) { + if ($person->isEmpty()) { throw new Exception('Person not found'); } @@ -7044,7 +7044,7 @@ public function testOneToOneTwoWayRelationship(): void Query::select(['*', 'city.name']) ]); - if (!$country instanceof Document) { + if ($country->isEmpty()) { throw new Exception('Country not found'); } @@ -7618,7 +7618,7 @@ public function testOneToManyOneWayRelationship(): void Query::select(['*', 'albums.name']) ]); - if (!$artist instanceof Document) { + if ($artist->isEmpty()) { $this->fail('Artist not found'); } @@ -8051,7 +8051,7 @@ public function testOneToManyTwoWayRelationship(): void Query::select(['*', 'accounts.name']) ]); - if (!$customer instanceof Document) { + if ($customer->isEmpty()) { throw new Exception('Customer not found'); } @@ -8463,7 +8463,7 @@ public function testManyToOneOneWayRelationship(): void Query::select(['*', 'movie.name']) ]); - if (!$review instanceof Document) { + if ($review->isEmpty()) { throw new Exception('Review not found'); } @@ -8840,7 +8840,7 @@ public function testManyToOneTwoWayRelationship(): void Query::select(['*', 'store.name']) ]); - if (!$product instanceof Document) { + if ($product->isEmpty()) { throw new Exception('Product not found'); } @@ -9222,7 +9222,7 @@ public function testManyToManyOneWayRelationship(): void Query::select(['*', 'songs.name']) ]); - if (!$playlist instanceof Document) { + if ($playlist->isEmpty()) { throw new Exception('Playlist not found'); } @@ -9604,7 +9604,7 @@ public function testManyToManyTwoWayRelationship(): void Query::select(['*', 'classes.name']) ]); - if (!$student instanceof Document) { + if ($student->isEmpty()) { throw new Exception('Student not found'); } @@ -9908,7 +9908,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', 'models.name']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -9930,7 +9930,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', '$id']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -9945,7 +9945,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', '$internalId']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -9960,7 +9960,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', '$collection']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -9975,7 +9975,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', '$createdAt']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -9990,7 +9990,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', '$updatedAt']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -10005,7 +10005,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name', '$permissions']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -10021,7 +10021,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['*', 'models.year']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -10037,7 +10037,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['*', 'models.*']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -10054,7 +10054,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['models.*']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } @@ -10070,7 +10070,7 @@ public function testSelectRelationshipAttributes(): void Query::select(['name']), ]); - if (!$make instanceof Document) { + if ($make->isEmpty()) { throw new Exception('Make not found'); } From 7fc54021d22e80a3e29105b77cdfc4d21ff9336e Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 7 Oct 2024 11:06:43 +0900 Subject: [PATCH 076/256] Update src/Database/Database.php Co-authored-by: Jake Barnby --- src/Database/Database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 008aedc97..b496f1e16 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5187,9 +5187,9 @@ public function findOne(string $collection, array $queries = []): Document if (!$found) { return new Document(); - } else { - return $found; } + + return $found; } /** From 923260ac25780b3e60c255a5529da4cc440ae174 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 7 Oct 2024 11:09:46 +0900 Subject: [PATCH 077/256] Run Linter --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index b496f1e16..11e8eab01 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5188,7 +5188,7 @@ public function findOne(string $collection, array $queries = []): Document if (!$found) { return new Document(); } - + return $found; } From 238883ab8a80cb107c5dc75092178df6a077dae9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 13:34:20 +1300 Subject: [PATCH 078/256] Exception simplification --- src/Database/Adapter/MariaDB.php | 48 ++++++------------------------- src/Database/Adapter/MySQL.php | 23 +-------------- src/Database/Adapter/Postgres.php | 11 ++----- 3 files changed, 11 insertions(+), 71 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index bef86ebdc..ca9f0eba9 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -948,16 +948,8 @@ public function createDocument(string $collection, Document $document): Document if (isset($stmtPermissions)) { $stmtPermissions->execute(); } - } catch (\Throwable $e) { - if ($e instanceof PDOException) { - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage(), previous: $e); - } - } - - throw $e; + } catch (PDOException $e) { + throw $this->processException($e); } return $document; @@ -1079,16 +1071,8 @@ public function createDocuments(string $collection, array $documents, int $batch $stmtPermissions?->execute(); } } - } catch (\Throwable $e) { - if ($e instanceof PDOException) { - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage(), previous: $e); - } - } - - throw $e; + } catch (PDOException $e) { + throw $this->processException($e); } foreach ($documents as $document) { @@ -1337,16 +1321,8 @@ public function updateDocument(string $collection, string $id, Document $documen $stmtAddPermissions->execute(); } - } catch (\Throwable $e) { - if ($e instanceof PDOException) { - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage(), previous: $e); - } - } - - throw $e; + } catch (PDOException $e) { + throw $this->processException($e); } return $document; @@ -1594,16 +1570,8 @@ public function updateDocuments(string $collection, array $documents, int $batch $stmtAddPermissions->execute(); } } - } catch (\Throwable $e) { - if ($e instanceof PDOException) { - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage(), previous: $e); - } - } - - throw $e; + } catch (PDOException $e) { + $this->processException($e); } return $documents; diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index c0ac85f88..504dec2d1 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -91,27 +91,6 @@ protected function processException(PDOException $e): \Exception return new TimeoutException($e->getMessage(), $e->getCode(), $e); } - // Duplicate table - if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } - - // Duplicate column - if ($e->getCode() === '42S21' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1060) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } - - // Duplicate index - if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1061) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); - } - - // Data is too big for column resize - if (($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) || - ($e->getCode() === '01000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1265)) { - return new TruncateException('Resize would result in data truncation', $e->getCode(), $e); - } - - return $e; + return parent::processException($e); } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index c7ae88be9..de42e10a8 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1260,6 +1260,7 @@ public function updateDocument(string $collection, string $id, Document $documen * @return array * @throws DatabaseException * @throws DuplicateException + * @throws Exception */ public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { @@ -1491,15 +1492,7 @@ public function updateDocuments(string $collection, array $documents, int $batch return $documents; } catch (PDOException $e) { - - // Must be a switch for loose match - switch ($e->getCode()) { - case 1062: - case 23505: - throw new DuplicateException('Duplicated document: ' . $e->getMessage()); - default: - throw $e; - } + throw $this->processException($e); } } From 9105080161087ea5f14a88405315ad7b124d2677 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 13:42:23 +1300 Subject: [PATCH 079/256] Remove redundant get document --- dev/xdebug.ini | 2 +- src/Database/Database.php | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/dev/xdebug.ini b/dev/xdebug.ini index 34ec0e4b6..9fdfa2d76 100644 --- a/dev/xdebug.ini +++ b/dev/xdebug.ini @@ -3,10 +3,10 @@ zend_extension = xdebug.so [xdebug] xdebug.mode = develop,debug,profile xdebug.start_with_request = yes +xdebug.use_compression=false xdebug.client_host=host.docker.internal xdebug.client_port = 9003 xdebug.log = /tmp/xdebug.log -xdebug.use_compression=false xdebug.var_display_max_depth = 10 xdebug.var_display_max_children = 256 diff --git a/src/Database/Database.php b/src/Database/Database.php index 0b631d409..4cebb9a91 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4516,12 +4516,9 @@ public function decreaseDocumentAttribute(string $collection, string $id, string * @throws ConflictException * @throws DatabaseException * @throws RestrictedException - * @throws StructureException */ public function deleteDocument(string $collection, string $id): bool { - $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); - $collection = $this->silent(fn () => $this->getCollection($collection)); $deleted = $this->withTransaction(function () use ($collection, $id, &$document) { From fde34787ab35383a33ded502a3b5327766cc0be2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 13:46:48 +1300 Subject: [PATCH 080/256] Lint/check fixes --- composer.json | 2 +- src/Database/Adapter/MySQL.php | 2 -- src/Database/Adapter/Postgres.php | 6 +----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index d0097c5d5..a99e03468 100755 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ ], "lint": "./vendor/bin/pint --test", "format": "./vendor/bin/pint", - "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 512M", + "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G", "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, "require": { diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 504dec2d1..7564d5a51 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -5,9 +5,7 @@ use PDOException; use Utopia\Database\Database; use Utopia\Database\Exception as DatabaseException; -use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; -use Utopia\Database\Exception\Truncate as TruncateException; class MySQL extends MariaDB { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index de42e10a8..f6ae3a6fc 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -904,9 +904,8 @@ public function createDocument(string $collection, Document $document): Document if (isset($stmtPermissions)) { $stmtPermissions->execute(); } - } catch (Throwable $e) { + } catch (PDOException $e) { throw $this->processException($e); - } return $document; @@ -1007,9 +1006,6 @@ public function createDocuments(string $collection, array $documents, int $batch $stmtPermissions?->execute(); } } - - return $documents; - } catch (PDOException $e) { throw $this->processException($e); } From 790b870631a8061a335619d2ccb163ccc05235cf Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 13:54:06 +1300 Subject: [PATCH 081/256] Fix merge --- src/Database/Adapter/Postgres.php | 1 - src/Database/Adapter/SQL.php | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index f6ae3a6fc..29a6aee64 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -5,7 +5,6 @@ use Exception; use PDO; use PDOException; -use Throwable; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 80a3152f2..1e1bfaebc 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -211,10 +211,6 @@ public function getDocument(string $collection, string $id, array $queries = [], $sql .= " {$forUpdate}"; } - if ($this->getSupportForUpdateLock()) { - $sql .= " {$forUpdate}"; - } - $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_uid', $id); From 68f5bf6fc8838ed4a589f76842c6dbc4208adf4e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 14:56:02 +1300 Subject: [PATCH 082/256] Fix tests --- src/Database/Adapter/MariaDB.php | 5 +++++ src/Database/Adapter/Postgres.php | 5 +++++ src/Database/Adapter/SQLite.php | 2 +- tests/e2e/Adapter/Base.php | 17 +++++++++++++---- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ca9f0eba9..9dba28900 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2306,6 +2306,11 @@ protected function processException(PDOException $e): \Exception return new DuplicateException($e->getMessage(), $e->getCode(), $e); } + // Duplicate row + if ($e->getCode() === '23000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1062) { + return new DuplicateException($e->getMessage(), $e->getCode(), $e); + } + // Data is too big for column resize if (($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1406) || ($e->getCode() === '01000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1265)) { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 29a6aee64..15f919dd9 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2272,6 +2272,11 @@ protected function processException(PDOException $e): \Exception return new DuplicateException($e->getMessage(), $e->getCode(), $e); } + // Duplicate row + if ($e->getCode() === '23505' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { + return new DuplicateException($e->getMessage(), $e->getCode(), $e); + } + // Data is too big for column resize if ($e->getCode() === '22001' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { return new TruncateException('Resize would result in data truncation', $e->getCode(), $e); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 1eac1d516..7db1760ba 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -263,7 +263,7 @@ public function deleteCollection(string $id): bool { $id = $this->filter($id); - $sql = "DROP TABLE IF EXISTS `{$this->getSQLTable($id)}`"; + $sql = "DROP TABLE IF EXISTS {$this->getSQLTable($id)}"; $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); $this->getPDO() diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 9d8559b1d..e04213933 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15612,7 +15612,7 @@ public function testSharedTables(): void $database->setDatabase($this->testDatabase); } - public function testSharedTablesDuplicatesDontThrow(): void + public function testSharedTablesDuplicates(): void { $database = static::getDatabase(); @@ -15629,7 +15629,7 @@ public function testSharedTablesDuplicatesDontThrow(): void ->setDatabase('sharedTables') ->setNamespace('') ->setSharedTables(true) - ->setTenant(1) + ->setTenant(null) ->create(); // Create collection @@ -15645,8 +15645,17 @@ public function testSharedTablesDuplicatesDontThrow(): void // Ignore } - $database->createAttribute('duplicates', 'name', Database::VAR_STRING, 10, false); - $database->createIndex('duplicates', 'nameIndex', Database::INDEX_KEY, ['name']); + try { + $database->createAttribute('duplicates', 'name', Database::VAR_STRING, 10, false); + } catch (DuplicateException) { + // Ignore + } + + try { + $database->createIndex('duplicates', 'nameIndex', Database::INDEX_KEY, ['name']); + } catch (DuplicateException) { + // Ignore + } $collection = $database->getCollection('duplicates'); $this->assertEquals(1, \count($collection->getAttribute('attributes'))); From 74d7f8db9a4670c470681c2d4f6ce70e61bdf7cd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 16:53:56 +1300 Subject: [PATCH 083/256] Fix cache not flushed on db delete --- src/Database/Database.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 4cebb9a91..00bf9bd9c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1076,6 +1076,8 @@ public function delete(?string $database = null): bool 'deleted' => $deleted ]); + $this->cache->flush(); + return $deleted; } From e06001b5785ced2f2994f885680c958d0a91b7a1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 16:54:15 +1300 Subject: [PATCH 084/256] Fix upgrade read on source/dest mirror --- src/Database/Mirror.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index 6ff3860a7..bb0b44ca8 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -901,7 +901,7 @@ protected function getUpgradeStatus(string $collection): ?Document return Authorization::skip(function () use ($collection) { try { - return $this->getDocument('upgrades', $collection); + return $this->source->getDocument('upgrades', $collection); } catch (\Throwable) { return; } From ec47a5e31126aba2c79d8d2e66aa99b6b309da52 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 14 Oct 2024 18:01:22 +1300 Subject: [PATCH 085/256] Fix max allowed packet on mirror --- docker-compose.yml | 1 + src/Database/Adapter/MariaDB.php | 2 +- src/Database/Database.php | 2 +- tests/e2e/Adapter/Base.php | 3 +-- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 82076ebfe..76614e18b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,6 +50,7 @@ services: mariadb-mirror: image: mariadb:10.11 container_name: utopia-mariadb-mirror + command: mariadbd --max_allowed_packet=1G networks: - database ports: diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 9dba28900..7465db0ec 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1816,7 +1816,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $sql = " SELECT {$this->getAttributeProjection($selections, 'table_main')} - FROM {$this->getSQLTable($name)} as table_main + FROM {$this->getSQLTable($name)} AS table_main {$sqlWhere} {$sqlOrder} {$sqlLimit}; diff --git a/src/Database/Database.php b/src/Database/Database.php index 00bf9bd9c..080dea1f7 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1722,7 +1722,7 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de /** * Update Attribute. This method is for updating data that causes underlying structure to change. Check out other updateAttribute methods if you are looking for metadata adjustments. - * To update attribute key (ID), use renameAttribute instead. + * * @param string $collection * @param string $id * @param string|null $type diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index e04213933..2ddeb9107 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6123,8 +6123,7 @@ public function testUpdateAttributeSize(): void $document = $this->updateStringAttributeSize(65536, $document); // 65536-16777216 to PHP_INT_MAX or adapter limit - $maxStringSize = 16777217; - $document = $this->updateStringAttributeSize($maxStringSize, $document); + $document = $this->updateStringAttributeSize(16777217, $document); // Test going down in size with data that is too big (Expect Failure) try { From 58c7c6843a3010e801d0c48a30c96ec0b78409ec Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 14:00:52 +1300 Subject: [PATCH 086/256] Improve delegation --- src/Database/Mirror.php | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index bb0b44ca8..f607ee3a4 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -104,35 +104,35 @@ protected function delegate(string $method, array $args = []): mixed public function setDatabase(string $name): static { - $this->delegate('setDatabase', [$name]); + $this->delegate(__FUNCTION__, \func_get_args()); return $this; } public function setNamespace(string $namespace): static { - $this->delegate('setNamespace', [$namespace]); + $this->delegate(__FUNCTION__, \func_get_args()); return $this; } public function setSharedTables(bool $sharedTables): static { - $this->delegate('setSharedTables', [$sharedTables]); + $this->delegate(__FUNCTION__, \func_get_args()); return $this; } public function setTenant(?int $tenant): static { - $this->delegate('setTenant', [$tenant]); + $this->delegate(__FUNCTION__, \func_get_args()); return $this; } public function setPreserveDates(bool $preserve): static { - $this->delegate('setPreserveDates', [$preserve]); + $this->delegate(__FUNCTION__, \func_get_args()); $this->preserveDates = $preserve; @@ -141,7 +141,7 @@ public function setPreserveDates(bool $preserve): static public function enableValidation(): static { - $this->delegate('enableValidation'); + $this->delegate(__FUNCTION__); $this->validate = true; @@ -150,7 +150,7 @@ public function enableValidation(): static public function disableValidation(): static { - $this->delegate('disableValidation'); + $this->delegate(__FUNCTION__); $this->validate = false; @@ -176,22 +176,22 @@ public function silent(callable $callback, array $listeners = null): mixed public function withRequestTimestamp(?\DateTime $requestTimestamp, callable $callback): mixed { - return $this->delegate('withRequestTimestamp', [$requestTimestamp, $callback]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function exists(?string $database = null, ?string $collection = null): bool { - return $this->delegate('exists', [$database, $collection]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function create(?string $database = null): bool { - return $this->delegate('create', [$database]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function delete(?string $database = null): bool { - return $this->delegate('delete', [$database]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document @@ -760,32 +760,32 @@ public function deleteDocument(string $collection, string $id): bool public function updateAttributeRequired(string $collection, string $id, bool $required): Document { - return $this->delegate('updateAttributeRequired', [$collection, $id, $required]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function updateAttributeFormat(string $collection, string $id, string $format): Document { - return $this->delegate('updateAttributeFormat', [$collection, $id, $format]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function updateAttributeFormatOptions(string $collection, string $id, array $formatOptions): Document { - return $this->delegate('updateAttributeFormatOptions', [$collection, $id, $formatOptions]); + return $this->delegate(__FUNCTION__, [$collection, $id, $formatOptions]); } public function updateAttributeFilters(string $collection, string $id, array $filters): Document { - return $this->delegate('updateAttributeFilters', [$collection, $id, $filters]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function updateAttributeDefault(string $collection, string $id, mixed $default = null): Document { - return $this->delegate('updateAttributeDefault', [$collection, $id, $default]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function renameAttribute(string $collection, string $old, string $new): bool { - return $this->delegate('renameAttribute', [$collection, $old, $new]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function createRelationship( @@ -797,7 +797,7 @@ public function createRelationship( ?string $twoWayKey = null, string $onDelete = Database::RELATION_MUTATE_RESTRICT ): bool { - return $this->delegate('createRelationship', [$collection, $relatedCollection, $type, $twoWay, $id, $twoWayKey, $onDelete]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function updateRelationship( @@ -808,28 +808,28 @@ public function updateRelationship( ?bool $twoWay = null, ?string $onDelete = null ): bool { - return $this->delegate('updateRelationship', [$collection, $id, $newKey, $newTwoWayKey, $twoWay, $onDelete]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function deleteRelationship(string $collection, string $id): bool { - return $this->delegate('deleteRelationship', [$collection, $id]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function renameIndex(string $collection, string $old, string $new): bool { - return $this->delegate('renameIndex', [$collection, $old, $new]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool { - return $this->delegate('increaseDocumentAttribute', [$collection, $id, $attribute, $value, $max]); + return $this->delegate(__FUNCTION__, \func_get_args()); } public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool { - return $this->delegate('decreaseDocumentAttribute', [$collection, $id, $attribute, $value, $min]); + return $this->delegate(__FUNCTION__, \func_get_args()); } /** @@ -903,7 +903,7 @@ protected function getUpgradeStatus(string $collection): ?Document try { return $this->source->getDocument('upgrades', $collection); } catch (\Throwable) { - return; + return null; } }); } From 6639898d2aa6e58fd2c78f6a0bc3ff2bf799e4b4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 14:01:14 +1300 Subject: [PATCH 087/256] Fix spelling --- Dockerfile | 2 +- tests/e2e/Adapter/Base.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 87fca91e3..f7985eba1 100755 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer install \ --no-scripts \ --prefer-dist -FROM php:8.3.10-cli-alpine3.20 as compile +FROM php:8.3.10-cli-alpine3.20 AS compile ENV PHP_REDIS_VERSION="6.0.2" \ PHP_SWOOLE_VERSION="v5.1.3" \ diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 2ddeb9107..d569458ee 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6446,11 +6446,11 @@ public function testNoInvalidKeysWithRelationships(): void } static::getDatabase()->createCollection('species'); static::getDatabase()->createCollection('creatures'); - static::getDatabase()->createCollection('characterstics'); + static::getDatabase()->createCollection('characteristics'); static::getDatabase()->createAttribute('species', 'name', Database::VAR_STRING, 255, true); static::getDatabase()->createAttribute('creatures', 'name', Database::VAR_STRING, 255, true); - static::getDatabase()->createAttribute('characterstics', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('characteristics', 'name', Database::VAR_STRING, 255, true); static::getDatabase()->createRelationship( collection: 'species', @@ -6462,10 +6462,10 @@ public function testNoInvalidKeysWithRelationships(): void ); static::getDatabase()->createRelationship( collection: 'creatures', - relatedCollection: 'characterstics', + relatedCollection: 'characteristics', type: Database::RELATION_ONE_TO_ONE, twoWay: true, - id: 'characterstic', + id: 'characteristic', twoWayKey:'creature' ); @@ -6481,7 +6481,7 @@ public function testNoInvalidKeysWithRelationships(): void Permission::read(Role::any()), ], 'name' => 'Dog', - 'characterstic' => [ + 'characteristic' => [ '$id' => ID::custom('1'), '$permissions' => [ Permission::read(Role::any()), @@ -6497,10 +6497,10 @@ public function testNoInvalidKeysWithRelationships(): void 'creature' => [ '$id' => ID::custom('1'), '$collection' => 'creatures', - 'characterstic' => [ + 'characteristic' => [ '$id' => ID::custom('1'), 'name' => 'active', - '$collection' => 'characterstics', + '$collection' => 'characteristics', ] ] ])); From a9de51b4817c26eaf7d9da8c494ca4dc07292c75 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 14:04:08 +1300 Subject: [PATCH 088/256] Add redis mirror --- docker-compose.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 76614e18b..70df221eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -102,12 +102,20 @@ services: - SYS_NICE redis: - image: redis:6.0-alpine + image: redis:7.4.1-alpine3.20 container_name: utopia-redis ports: - "8706:6379" networks: - database + redis-mirror: + image: redis:7.4.1-alpine3.20 + container_name: utopia-redis + ports: + - "8707:6379" + networks: + - database + networks: database: From 30e470fdebb1f2f027ed3816493abd78457f8ed5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 14:16:44 +1300 Subject: [PATCH 089/256] Use mirror cache for mirror db --- docker-compose.yml | 31 +++++++++++++++++++++---------- tests/e2e/Adapter/Base.php | 7 ++----- tests/e2e/Adapter/MirrorTest.php | 6 +++++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 70df221eb..1672fbca1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,17 +21,28 @@ services: container_name: utopia-adminer restart: always ports: - - "8760:8080" + - "8700:8080" networks: - database postgres: - image: postgres:13 + image: postgres:16.4 container_name: utopia-postgres networks: - database ports: - - "8700:5432" + - "8701:5432" + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + + postgres-mirror: + image: postgres:16.4 + container_name: utopia-postgres-mirror + networks: + - database + ports: + - "8702:5432" environment: POSTGRES_USER: root POSTGRES_PASSWORD: password @@ -43,7 +54,7 @@ services: networks: - database ports: - - "8701:3306" + - "8703:3306" environment: - MYSQL_ROOT_PASSWORD=password @@ -64,7 +75,7 @@ services: networks: - database ports: - - "8702:27017" + - "8705:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example @@ -75,7 +86,7 @@ services: networks: - database ports: - - "8703:3307" + - "8706:3307" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: default @@ -91,7 +102,7 @@ services: networks: - database ports: - - "8705:3307" + - "8707:3307" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: default @@ -105,15 +116,15 @@ services: image: redis:7.4.1-alpine3.20 container_name: utopia-redis ports: - - "8706:6379" + - "8708:6379" networks: - database redis-mirror: image: redis:7.4.1-alpine3.20 - container_name: utopia-redis + container_name: utopia-redis-mirror ports: - - "8707:6379" + - "8709:6379" networks: - database diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index d569458ee..26bba07f1 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6354,14 +6354,9 @@ public function testKeywords(): void $this->assertCount(1, $documents); $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); - $collection = $database->deleteCollection($collectionName); $this->assertTrue($collection); - - // TODO: updateAttribute name tests } - - // TODO: Index name tests } public function testWritePermissions(): void @@ -6504,7 +6499,9 @@ public function testNoInvalidKeysWithRelationships(): void ] ] ])); + $updatedSpecies = static::getDatabase()->getDocument('species', $species->getId()); + $this->assertEquals($species, $updatedSpecies); } diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php index 8a0bef837..efcfb04c8 100644 --- a/tests/e2e/Adapter/MirrorTest.php +++ b/tests/e2e/Adapter/MirrorTest.php @@ -56,8 +56,12 @@ protected static function getDatabase(bool $fresh = false): Mirror $mirrorPass = 'password'; $mirrorPdo = new PDO("mysql:host={$mirrorHost};port={$mirrorPort};charset=utf8mb4", $mirrorUser, $mirrorPass, MariaDB::getPDOAttributes()); + $mirrorRedis = new Redis(); + $mirrorRedis->connect('redis-mirror'); + $mirrorRedis->flushAll(); + $mirrorCache = new Cache(new RedisAdapter($mirrorRedis)); - self::$destination = new Database(new MariaDB($mirrorPdo), $cache); + self::$destination = new Database(new MariaDB($mirrorPdo), $mirrorCache); $database = new Mirror(self::$source, self::$destination); From 781b421fca340c0b4628407eb66c502061172fc2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 14:23:10 +1300 Subject: [PATCH 090/256] Lint --- src/Database/Mirror.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index f607ee3a4..2f9f1d186 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -903,7 +903,7 @@ protected function getUpgradeStatus(string $collection): ?Document try { return $this->source->getDocument('upgrades', $collection); } catch (\Throwable) { - return null; + return; } }); } From 651cb4ffbcb743676a784b0c118f38f3bafff2e5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 15:30:09 +1300 Subject: [PATCH 091/256] Fix SQLite tests --- src/Database/Adapter/SQLite.php | 22 +++++++++++----------- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 7db1760ba..cb75411ac 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -280,6 +280,17 @@ public function deleteCollection(string $id): bool return true; } + /** + * Analyze a collection updating it's metadata on the database engine + * + * @param string $collection + * @return bool + */ + public function analyzeCollection(string $collection): bool + { + return false; + } + /** * Update Attribute * @@ -1393,15 +1404,4 @@ protected function processException(PDOException $e): \Exception return $e; } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - public function analyzeCollection(string $collection): bool - { - return false; - } } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 26bba07f1..45eb64859 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15612,7 +15612,7 @@ public function testSharedTablesDuplicates(): void { $database = static::getDatabase(); - if (!$database->getAdapter()->getSupportForAttributes()) { + if (!$database->getAdapter()->getSupportForSchemas()) { $this->expectNotToPerformAssertions(); return; } From b63c999e3a0617779e1f84480267d7d18622b542 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 16:52:13 +1300 Subject: [PATCH 092/256] Fix order of operations issues --- tests/e2e/Adapter/MirrorTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php index efcfb04c8..ee34386f9 100644 --- a/tests/e2e/Adapter/MirrorTest.php +++ b/tests/e2e/Adapter/MirrorTest.php @@ -65,6 +65,26 @@ protected static function getDatabase(bool $fresh = false): Mirror $database = new Mirror(self::$source, self::$destination); + // Handle cases where the source and destination databases are not in sync because of previous tests + if ($database->getSource()->exists('schema1')) { + $database->getSource()->setDatabase('schema1')->delete(); + } + if ($database->getDestination()->exists('schema1')) { + $database->getDestination()->setDatabase('schema1')->delete(); + } + if ($database->getSource()->exists('schema2')) { + $database->getSource()->setDatabase('schema2')->delete(); + } + if ($database->getDestination()->exists('schema2')) { + $database->getDestination()->setDatabase('schema2')->delete(); + } + if ($database->getSource()->exists('sharedTables')) { + $database->getSource()->setDatabase('sharedTables')->delete(); + } + if ($database->getDestination()->exists('sharedTables')) { + $database->getDestination()->setDatabase('sharedTables')->delete(); + } + $database ->setDatabase('utopiaTests') ->setNamespace(static::$namespace = 'myapp_' . uniqid()); From 8bb5793b6e252e2ec840b7056d74e65767c287e8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 16:52:20 +1300 Subject: [PATCH 093/256] Fix mongo tests --- src/Database/Adapter/Mongo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index b15aef596..19b33024c 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1593,7 +1593,7 @@ public function getLimitForIndexes(): int */ public function getSupportForSchemas(): bool { - return true; + return false; } /** From 78f0e9d08b5f414b303a6116d2e5cb379b312e97 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 18:41:32 +1300 Subject: [PATCH 094/256] Bail out of commit if no active transaction --- src/Database/Adapter/SQL.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 1e1bfaebc..34dcea386 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -67,6 +67,11 @@ public function commitTransaction(): bool return true; } + if (!$this->getPDO()->inTransaction()) { + $this->inTransaction = 0; + return false; + } + try { $result = $this->getPDO()->commit(); } catch (PDOException $e) { From efd0f4e8fb5d158486aec0e6fea9561facf0daad Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 15 Oct 2024 18:50:09 +1300 Subject: [PATCH 095/256] Fix stan --- src/Database/Adapter/SQL.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 34dcea386..ae2dbea22 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -68,6 +68,7 @@ public function commitTransaction(): bool } if (!$this->getPDO()->inTransaction()) { + // Implicit commit occurred $this->inTransaction = 0; return false; } @@ -75,18 +76,12 @@ public function commitTransaction(): bool try { $result = $this->getPDO()->commit(); } catch (PDOException $e) { - if ($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { $this->inTransaction--; } if (!$result) { - if ($this->getPDO()->inTransaction()) { - $this->getPDO()->rollBack(); - } throw new DatabaseException('Failed to commit transaction'); } From 7c15e2ba06e82d7daeb4edbf43564b435324a7d4 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 15 Oct 2024 15:20:07 +0300 Subject: [PATCH 096/256] max query fix --- src/Database/Database.php | 20 +++++++++++++++++--- src/Database/Validator/Queries/Documents.php | 4 ++-- tests/e2e/Adapter/Base.php | 20 ++++++++++++++++++++ tests/unit/Validator/Query/FilterTest.php | 11 +++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index a2d36e669..9ac729152 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -326,6 +326,8 @@ class Database protected bool $preserveDates = false; + protected int $maxQueryValues = 100; + /** * Stack of collection IDs when creating or updating related documents * @var array @@ -832,6 +834,18 @@ public function setPreserveDates(bool $preserve): self return $this; } + public function setMaxQueryValues(int $max): self + { + $this->maxQueryValues = $max; + + return $this; + } + + public function getMaxQueryValues(): int + { + return$this->maxQueryValues; + } + /** * Get list of keywords that cannot be used * @@ -5055,7 +5069,7 @@ public function find(string $collection, array $queries = []): array $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator($attributes, $indexes, maxValuesCount: $this->maxQueryValues); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5230,7 +5244,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator($attributes, $indexes, maxValuesCount: $this->maxQueryValues); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5276,7 +5290,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator($attributes, $indexes, maxValuesCount: $this->maxQueryValues); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index 0d1dc2384..d308f874d 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -22,7 +22,7 @@ class Documents extends IndexedQueries * @param array $indexes * @throws Exception */ - public function __construct(array $attributes, array $indexes) + public function __construct(array $attributes, array $indexes, int $maxValuesCount = 100) { $attributes[] = new Document([ '$id' => '$id', @@ -53,7 +53,7 @@ public function __construct(array $attributes, array $indexes) new Limit(), new Offset(), new Cursor(), - new Filter($attributes), + new Filter($attributes, $maxValuesCount), new Order($attributes), new Select($attributes), ]; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a93835646..49f73aeb9 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2307,6 +2307,26 @@ public function testListDocumentSearch(): void $this->assertEquals(1, count($documents)); } + public function testMaxQuwriesValuse(): void + { + $max = static::getDatabase()->getMaxQueryValues(); + + static::getDatabase()->setMaxQueryValues(5); + + try { + static::getDatabase()->find( + 'documents', + [Query::equal('$id', [1, 2, 3, 4, 5, 6])] + ); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: Query on attribute has greater than 5 values: $id', $e->getMessage()); + } + + static::getDatabase()->setMaxQueryValues($max); + } + public function testEmptyTenant(): void { if (static::getDatabase()->getAdapter()->getSharedTables()) { diff --git a/tests/unit/Validator/Query/FilterTest.php b/tests/unit/Validator/Query/FilterTest.php index d9a2dbb38..1388dbd7c 100644 --- a/tests/unit/Validator/Query/FilterTest.php +++ b/tests/unit/Validator/Query/FilterTest.php @@ -103,4 +103,15 @@ public function testEmptyValues(): void $this->assertFalse($this->validator->isValid(Query::equal('string', []))); $this->assertEquals('Equal queries require at least one value.', $this->validator->getDescription()); } + + public function testMaxValuesCount(): void + { + $values = []; + for ($i = 1; $i <= 200; $i++) { + $values[] = $i; + } + + $this->assertFalse($this->validator->isValid(Query::equal('integer', $values))); + $this->assertEquals('Query on attribute has greater than 100 values: integer', $this->validator->getDescription()); + } } From fa10d5fdba1ad6c97a98a953152f4701f7cb3e84 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 16 Oct 2024 21:15:01 +1300 Subject: [PATCH 097/256] Allow skipping check for relationship existence --- src/Database/Database.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 9ac729152..84bc5c616 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -318,6 +318,8 @@ class Database protected bool $resolveRelationships = true; + protected bool $checkRelationshipsExist = true; + protected int $relationshipFetchDepth = 1; protected bool $filter = true; @@ -507,6 +509,18 @@ public function skipRelationships(callable $callback): mixed } } + public function skipRelationshipsExistCheck(callable $callback): mixed + { + $previous = $this->checkRelationshipsExist; + $this->checkRelationshipsExist = false; + + try { + return $callback(); + } finally { + $this->checkRelationshipsExist = $previous; + } + } + /** * Trigger callback for events * @@ -3642,7 +3656,7 @@ private function relateDocumentsById( // Get the related document, will be empty on permissions failure $related = $this->skipRelationships(fn () => $this->getDocument($relatedCollection->getId(), $relationId)); - if ($related->isEmpty()) { + if ($related->isEmpty() && $this->checkRelationshipsExist) { return; } @@ -3671,7 +3685,7 @@ private function relateDocumentsById( $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); $this->skipRelationships(fn () => $this->createDocument($junction, new Document([ - $key => $related->getId(), + $key => $relationId, $twoWayKey => $documentId, '$permissions' => [ Permission::read(Role::any()), From 9d0d9504e30593ce9717dcea4bfd9cb11fcb7dad Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 17 Oct 2024 21:14:43 +1300 Subject: [PATCH 098/256] Add not found exception --- src/Database/Adapter/MariaDB.php | 3 +- src/Database/Adapter/Postgres.php | 23 +++++++------ src/Database/Adapter/SQLite.php | 5 +-- src/Database/Database.php | 53 +++++++++++++++-------------- src/Database/Exception/NotFound.php | 9 +++++ 5 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 src/Database/Exception/NotFound.php diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 246079eed..1a2b2348c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -9,6 +9,7 @@ use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; @@ -709,7 +710,7 @@ public function createIndex(string $collection, string $id, string $type, array $collection = $this->getDocument(Database::METADATA, $collection); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $collectionAttributes = \json_decode($collection->getAttribute('attributes', []), true); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 2b9007000..ae2f34619 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -9,8 +9,9 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; -use Utopia\Database\Exception\Duplicate; -use Utopia\Database\Exception\Timeout; +use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\NotFound as NotFoundException; +use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; @@ -869,7 +870,7 @@ public function createDocument(string $collection, Document $document): Document } catch (Throwable $e) { switch ($e->getCode()) { case 23505: - throw new Duplicate('Duplicated document: ' . $e->getMessage()); + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); default: throw $e; } @@ -972,7 +973,7 @@ public function createDocuments(string $collection, array $documents, int $batch } catch (PDOException $e) { throw match ($e->getCode()) { - 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), default => $e, }; } @@ -1196,7 +1197,7 @@ public function updateDocument(string $collection, string $id, Document $documen switch ($e->getCode()) { case 1062: case 23505: - throw new Duplicate('Duplicated document: ' . $e->getMessage()); + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); default: throw $e; } @@ -1448,7 +1449,7 @@ public function updateDocuments(string $collection, array $documents, int $batch } catch (PDOException $e) { throw match ($e->getCode()) { - 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), + 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), default => $e, }; } @@ -1587,7 +1588,7 @@ public function deleteDocument(string $collection, string $id): bool * @return array * @throws Exception * @throws PDOException - * @throws Timeout + * @throws TimeoutException */ public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array { @@ -2212,8 +2213,8 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL /** * @param PDOException $e - * @throws Timeout - * @throws Duplicate + * @throws TimeoutException + * @throws DuplicateException */ protected function processException(PDOException $e): void { @@ -2222,11 +2223,11 @@ protected function processException(PDOException $e): void */ if ($e->getCode() === '57014' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - throw new Timeout($e->getMessage(), $e->getCode(), $e); + throw new TimeoutException($e->getMessage(), $e->getCode(), $e); } if ($e->getCode() === '42701' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - throw new Duplicate($e->getMessage(), $e->getCode(), $e); + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); } // Data is too big for column resize diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index bbe2bdfde..d18963c4f 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -10,6 +10,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Duplicate as DuplicateException; +use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Helpers\ID; @@ -307,7 +308,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $collection = $this->getDocument(Database::METADATA, $name); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $indexes = \json_decode($collection->getAttribute('indexes', []), true); @@ -349,7 +350,7 @@ public function renameIndex(string $collection, string $old, string $new): bool $collection = $this->getDocument(Database::METADATA, $collection); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $old = $this->filter($old); diff --git a/src/Database/Database.php b/src/Database/Database.php index 84bc5c616..621092dfd 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -9,6 +9,7 @@ use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; +use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; @@ -1155,14 +1156,14 @@ public function updateCollection(string $id, array $permissions, bool $documentS $collection = $this->silent(fn () => $this->getCollection($id)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ( $this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant() ) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $collection @@ -1252,11 +1253,11 @@ public function getSizeOfCollection(string $collection): int $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } return $this->adapter->getSizeOfCollection($collection->getId()); @@ -1278,11 +1279,11 @@ public function getSizeOfCollectionOnDisk(string $collection): int $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } return $this->adapter->getSizeOfCollectionOnDisk($collection->getId()); @@ -1305,11 +1306,11 @@ public function deleteCollection(string $id): bool $collection = $this->silent(fn () => $this->getDocument(self::METADATA, $id)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $relationships = \array_filter( @@ -1369,11 +1370,11 @@ public function createAttribute(string $collection, string $id, string $type, in $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } // Attribute IDs are case insensitive @@ -1569,7 +1570,7 @@ protected function updateIndexMeta(string $collection, string $id, callable $upd $index = \array_search($id, \array_map(fn ($index) => $index['$id'], $indexes)); if ($index === false) { - throw new DatabaseException('Index not found'); + throw new NotFoundException('Index not found'); } // Execute update from callback @@ -1612,7 +1613,7 @@ protected function updateAttributeMeta(string $collection, string $id, callable $index = \array_search($id, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); if ($index === false) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } // Execute update from callback @@ -1930,7 +1931,7 @@ public function deleteAttribute(string $collection, string $id): bool } if (\is_null($attribute)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } if ($attribute['type'] === self::VAR_RELATIONSHIP) { @@ -1996,7 +1997,7 @@ public function renameAttribute(string $collection, string $old, string $new): b $attribute = \in_array($old, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); if ($attribute === false) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $attributeNew = \in_array($new, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); @@ -2070,13 +2071,13 @@ public function createRelationship( $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $relatedCollection = $this->silent(fn () => $this->getCollection($relatedCollection)); if ($relatedCollection->isEmpty()) { - throw new DatabaseException('Related collection not found'); + throw new NotFoundException('Related collection not found'); } $id ??= $relatedCollection->getId(); @@ -2281,7 +2282,7 @@ public function updateRelationship( $attributeIndex = array_search($id, array_map(fn ($attribute) => $attribute['$id'], $attributes)); if ($attributeIndex === false) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $attribute = $attributes[$attributeIndex]; @@ -2472,7 +2473,7 @@ public function deleteRelationship(string $collection, string $id): bool } if (\is_null($relationship)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $collection->setAttribute('attributes', \array_values($attributes)); @@ -2594,7 +2595,7 @@ public function renameIndex(string $collection, string $old, string $new): bool $index = \in_array($old, \array_map(fn ($index) => $index['$id'], $indexes)); if ($index === false) { - throw new DatabaseException('Index not found'); + throw new NotFoundException('Index not found'); } $indexNew = \in_array($new, \array_map(fn ($index) => $index['$id'], $indexes)); @@ -2819,7 +2820,7 @@ public function getDocument(string $collection, string $id, array $queries = [], } if (empty($collection)) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if (empty($id)) { @@ -2829,7 +2830,7 @@ public function getDocument(string $collection, string $id, array $queries = [], $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $attributes = $collection->getAttribute('attributes', []); @@ -4425,7 +4426,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string }); if (empty($attr)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $whiteList = [self::VAR_INTEGER, self::VAR_FLOAT]; @@ -4520,7 +4521,7 @@ public function decreaseDocumentAttribute(string $collection, string $id, string }); if (empty($attr)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $whiteList = [self::VAR_INTEGER, self::VAR_FLOAT]; @@ -5076,7 +5077,7 @@ public function find(string $collection, array $queries = []): array $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $attributes = $collection->getAttribute('attributes', []); @@ -5548,7 +5549,7 @@ public function casting(Document $collection, Document $document): Document protected function encodeAttribute(string $name, mixed $value, Document $document): mixed { if (!array_key_exists($name, self::$filters) && !array_key_exists($name, $this->instanceFilters)) { - throw new DatabaseException("Filter: {$name} not found"); + throw new NotFoundException("Filter: {$name} not found"); } try { @@ -5584,7 +5585,7 @@ protected function decodeAttribute(string $name, mixed $value, Document $documen } if (!array_key_exists($name, self::$filters) && !array_key_exists($name, $this->instanceFilters)) { - throw new DatabaseException('Filter not found'); + throw new NotFoundException('Filter not found'); } if (array_key_exists($name, $this->instanceFilters)) { diff --git a/src/Database/Exception/NotFound.php b/src/Database/Exception/NotFound.php new file mode 100644 index 000000000..a7e7168f6 --- /dev/null +++ b/src/Database/Exception/NotFound.php @@ -0,0 +1,9 @@ + Date: Thu, 17 Oct 2024 21:26:34 +1300 Subject: [PATCH 099/256] Fix stan --- composer.json | 2 +- src/Database/Adapter/Postgres.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index d0097c5d5..a99e03468 100755 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ ], "lint": "./vendor/bin/pint --test", "format": "./vendor/bin/pint", - "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 512M", + "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G", "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, "require": { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index ae2f34619..f59371cd8 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -10,7 +10,6 @@ use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; -use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; @@ -888,7 +887,7 @@ public function createDocument(string $collection, Document $document): Document * * @return array * - * @throws Duplicate + * @throws DuplicateException */ public function createDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { @@ -1215,7 +1214,7 @@ public function updateDocument(string $collection, string $id, Document $documen * * @return array * - * @throws Duplicate + * @throws DuplicateException */ public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { From 5b9462f8abc12c2c7ab2dac99aa6f68aa2e27365 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 22 Oct 2024 21:05:24 +1300 Subject: [PATCH 100/256] Add log dumps --- src/Database/Adapter/SQL.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index ae2dbea22..6cd92e155 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -35,10 +35,12 @@ public function startTransaction(): bool try { if ($this->inTransaction === 0) { if ($this->getPDO()->inTransaction()) { + \var_dump('Rolling back active transaction'); $this->getPDO()->rollBack(); } $result = $this->getPDO()->beginTransaction(); + \var_dump('Started transaction'); } else { $result = true; } @@ -48,10 +50,11 @@ public function startTransaction(): bool if (!$result) { throw new DatabaseException('Failed to start transaction'); + } else { + \var_dump('Incrementing transaction count'); + $this->inTransaction++; } - $this->inTransaction++; - return $result; } @@ -61,23 +64,28 @@ public function startTransaction(): bool public function commitTransaction(): bool { if ($this->inTransaction === 0) { + \var_dump('No transaction to commit'); return false; } elseif ($this->inTransaction > 1) { + \var_dump('Decrementing transaction count'); $this->inTransaction--; return true; } if (!$this->getPDO()->inTransaction()) { // Implicit commit occurred + \var_dump('Implicit commit occurred, resetting transaction count'); $this->inTransaction = 0; return false; } try { $result = $this->getPDO()->commit(); + \var_dump('Committed transaction'); } catch (PDOException $e) { throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { + \var_dump('Decrementing transaction count'); $this->inTransaction--; } @@ -94,14 +102,17 @@ public function commitTransaction(): bool public function rollbackTransaction(): bool { if ($this->inTransaction === 0) { + \var_dump('No transaction to rollback'); return false; } try { $result = $this->getPDO()->rollBack(); + \var_dump('Rolled back transaction'); } catch (PDOException $e) { throw new DatabaseException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { + \var_dump('Resetting transaction count'); $this->inTransaction = 0; } From 7bd224ae817776f078e7d22f7c8ee124d465e1cd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 22 Oct 2024 22:49:03 +1300 Subject: [PATCH 101/256] Debug --- src/Database/Adapter.php | 7 +++++-- src/Database/Adapter/SQL.php | 7 ++++++- src/Database/Database.php | 5 +++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 218694d4b..a68dd0e2f 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -286,13 +286,16 @@ public function inTransaction(): bool */ public function withTransaction(callable $callback): mixed { - $this->startTransaction(); - try { + \var_dump('Start'); + $this->startTransaction(); $result = $callback(); + \var_dump('Commit'); $this->commitTransaction(); + \var_dump('Return'); return $result; } catch (\Throwable $e) { + \var_dump('Rollback'); $this->rollbackTransaction(); throw $e; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 6cd92e155..cc899ceaf 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -34,7 +34,12 @@ public function startTransaction(): bool { try { if ($this->inTransaction === 0) { - if ($this->getPDO()->inTransaction()) { + $pdo = $this->getPDO(); + if ($pdo::class === 'Swoole\Database\PDOProxy' && $pdo->inTransaction()) { + \var_dump('Getting raw PDO from proxy'); + $pdo = $pdo->__getObject(); + } + if ($pdo->inTransaction()) { \var_dump('Rolling back active transaction'); $this->getPDO()->rollBack(); } diff --git a/src/Database/Database.php b/src/Database/Database.php index c542a9b37..1a9a5501a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3276,6 +3276,7 @@ public function createDocument(string $collection, Document $document): Document throw new StructureException($structure->getDescription()); } + \var_dump('before transaction createDocument', $collection, $document); $document = $this->withTransaction(function () use ($collection, $document) { if ($this->resolveRelationships) { $document = $this->silent(fn () => $this->createDocumentRelationships($collection, $document)); @@ -3342,6 +3343,7 @@ public function createDocuments(string $collection, array $documents, int $batch $documents[$key] = $document; } + \var_dump('before transaction createDocuments', $collection); $documents = $this->withTransaction(function () use ($collection, $documents, $batchSize) { return $this->adapter->createDocuments($collection->getId(), $documents, $batchSize); }); @@ -3703,6 +3705,7 @@ public function updateDocument(string $collection, string $id, Document $documen $collection = $this->silent(fn () => $this->getCollection($collection)); + \var_dump('before transaction updateDocument', $collection, $document); $document = $this->withTransaction(function () use ($collection, $id, $document) { $time = DateTime::now(); $old = Authorization::skip(fn () => $this->silent( @@ -3901,6 +3904,7 @@ public function updateDocuments(string $collection, array $documents, int $batch $collection = $this->silent(fn () => $this->getCollection($collection)); + \var_dump('before transaction updateDocuments', $collection); $documents = $this->withTransaction(function () use ($collection, $documents, $batchSize) { $time = DateTime::now(); @@ -4551,6 +4555,7 @@ public function deleteDocument(string $collection, string $id): bool { $collection = $this->silent(fn () => $this->getCollection($collection)); + \var_dump('before transaction deleteDocument', $collection, $id); $deleted = $this->withTransaction(function () use ($collection, $id, &$document) { $document = Authorization::skip(fn () => $this->silent( fn () => From df574633d6540984e5ae8efab806e3b493acca5b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 31 Oct 2024 18:26:25 +0200 Subject: [PATCH 102/256] Question? --- src/Database/Adapter/SQL.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index cc899ceaf..cc49313b4 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -91,6 +91,7 @@ public function commitTransaction(): bool throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { \var_dump('Decrementing transaction count'); + // Should we set this to 0 if we commit? $this->inTransaction--; } From 5b86dcfabfdcc9a6f7a0ac008ff42eb3ca482896 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 1 Nov 2024 20:21:14 +1300 Subject: [PATCH 103/256] Merge pull request #462 from utopia-php/feat-batch-update Feat bulk update # Conflicts: # src/Database/Adapter/MariaDB.php # src/Database/Adapter/Mongo.php # src/Database/Adapter/Postgres.php # src/Database/Database.php # tests/e2e/Adapter/Base.php --- composer.json | 2 +- composer.lock | 4 +- src/Database/Adapter.php | 20 +- src/Database/Adapter/MariaDB.php | 388 ++++++++-------- src/Database/Adapter/Mongo.php | 69 ++- src/Database/Adapter/Postgres.php | 381 ++++++++-------- src/Database/Adapter/SQL.php | 18 +- src/Database/Adapter/SQLite.php | 256 +---------- src/Database/Database.php | 138 +++--- src/Database/Validator/PartialStructure.php | 50 +++ src/Database/Validator/Structure.php | 54 +++ tests/e2e/Adapter/Base.php | 465 +++++++++++++++++--- 12 files changed, 1078 insertions(+), 767 deletions(-) create mode 100644 src/Database/Validator/PartialStructure.php diff --git a/composer.json b/composer.json index d0097c5d5..a99e03468 100755 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ ], "lint": "./vendor/bin/pint --test", "format": "./vendor/bin/pint", - "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 512M", + "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G", "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, "require": { diff --git a/composer.lock b/composer.lock index b01ed7d49..ee4eb4242 100644 --- a/composer.lock +++ b/composer.lock @@ -2585,7 +2585,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -2593,6 +2593,6 @@ "ext-mbstring": "*", "php": ">=8.0" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index c0f45fb8a..d174a8759 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -565,17 +565,19 @@ abstract public function createDocuments(string $collection, array $documents, i abstract public function updateDocument(string $collection, string $id, Document $document): Document; /** - * Update Documents in batches + * Update documents + * + * Updates all documents which match the given query. * * @param string $collection + * @param Document $updates * @param array $documents - * @param int $batchSize * - * @return array + * @return int * * @throws DatabaseException */ - abstract public function updateDocuments(string $collection, array $documents, int $batchSize): array; + abstract public function updateDocuments(string $collection, Document $updates, array $documents): int; /** * Delete Document @@ -600,10 +602,11 @@ abstract public function deleteDocument(string $collection, string $id): bool; * @param array $orderTypes * @param array $cursor * @param string $cursorDirection + * @param string $forPermission * * @return array */ - abstract public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array; + abstract public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array; /** * Sum an attribute @@ -747,6 +750,13 @@ abstract public function getSupportForRelationships(): bool; abstract public function getSupportForUpdateLock(): bool; + /** + * Are batch operations supported? + * + * @return bool + */ + abstract public function getSupportForBatchOperations(): bool; + /** * Is attribute resizing supported? * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 246079eed..3df5cdd2d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1306,260 +1306,259 @@ public function updateDocument(string $collection, string $id, Document $documen } /** - * Update Documents in batches + * Update documents + * + * Updates all documents which match the given query. * * @param string $collection + * @param Document $updates * @param array $documents - * @param int $batchSize * - * @return array + * @return int * - * @throws DuplicateException - * @throws \Throwable + * @throws DatabaseException */ - public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + public function updateDocuments(string $collection, Document $updates, array $documents): int { - if (empty($documents)) { - return $documents; + $attributes = $updates->getAttributes(); + + if (!empty($updates->getUpdatedAt())) { + $attributes['_updatedAt'] = $updates->getUpdatedAt(); } - try { - $name = $this->filter($collection); - $batches = \array_chunk($documents, max(1, $batchSize)); + if (!empty($updates->getPermissions())) { + $attributes['_permissions'] = json_encode($updates->getPermissions()); + } - foreach ($batches as $batch) { - $bindIndex = 0; - $batchKeys = []; - $bindValues = []; + if (empty($attributes)) { + return 0; + } - $removeQuery = ''; - $removeBindValues = []; + $name = $this->filter($collection); - $addQuery = ''; - $addBindValues = []; - /* @var $document Document */ - foreach ($batch as $index => $document) { - $attributes = $document->getAttributes(); - $attributes['_uid'] = $document->getId(); - $attributes['_createdAt'] = $document->getCreatedAt(); - $attributes['_updatedAt'] = $document->getUpdatedAt(); - $attributes['_permissions'] = json_encode($document->getPermissions()); + $columns = ''; - if ($this->sharedTables) { - $attributes['_tenant'] = $this->tenant; - } + $where = []; - $columns = \array_map(function ($attribute) { - return "`" . $this->filter($attribute) . "`"; - }, \array_keys($attributes)); + $ids = \array_map(fn ($document) => $document->getId(), $documents); + $where[] = "_uid IN (" . \implode(', ', \array_map(fn ($index) => ":_id_{$index}", \array_keys($ids))) . ")"; - $bindKeys = []; + if ($this->sharedTables) { + $where[] = "_tenant = :_tenant"; + } - foreach ($attributes as $value) { - if (\is_array($value)) { - $value = json_encode($value); - } - $value = (is_bool($value)) ? (int)$value : $value; - $bindKey = 'key_' . $bindIndex; - $bindKeys[] = ':' . $bindKey; - $bindValues[$bindKey] = $value; - $bindIndex++; - } + $sqlWhere = 'WHERE ' . implode(' AND ', $where); - $batchKeys[] = '(' . implode(', ', $bindKeys) . ')'; + $bindIndex = 0; + foreach ($attributes as $attribute => $value) { + $column = $this->filter($attribute); + $bindKey = 'key_' . $bindIndex; + $columns .= "`{$column}`" . '=:' . $bindKey; - // Permissions logic - $sql = " - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} - WHERE _document = :_uid - "; + if ($attribute !== \array_key_last($attributes)) { + $columns .= ','; + } - if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; - } + $bindIndex++; + } - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); + $sql = " + UPDATE {$this->getSQLTable($name)} + SET {$columns} + {$sqlWhere} + "; - $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $document->getId()); + $sql = $this->trigger(Database::EVENT_DOCUMENTS_UPDATE, $sql); + $stmt = $this->getPDO()->prepare($sql); - if ($this->sharedTables) { - $permissionsStmt->bindValue(':_tenant', $this->tenant); - } + if ($this->sharedTables) { + $stmt->bindValue(':_tenant', $this->tenant); + } - $permissionsStmt->execute(); - $permissions = $permissionsStmt->fetchAll(); + foreach ($ids as $id => $value) { + $stmt->bindValue(":_id_{$id}", $value); + } - $initial = []; - foreach (Database::PERMISSIONS as $type) { - $initial[$type] = []; - } + $attributeIndex = 0; + foreach ($attributes as $attribute => $value) { + if (is_array($value)) { + $value = json_encode($value); + } - $permissions = \array_reduce($permissions, function (array $carry, array $item) { - $carry[$item['_type']][] = $item['_permission']; - return $carry; - }, $initial); + $bindKey = 'key_' . $attributeIndex; + $value = (is_bool($value)) ? (int)$value : $value; + $stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value)); + $attributeIndex++; + } - // Get removed Permissions - $removals = []; - foreach (Database::PERMISSIONS as $type) { - $diff = array_diff($permissions[$type], $document->getPermissionsByType($type)); - if (!empty($diff)) { - $removals[$type] = $diff; - } - } + $stmt->execute(); + $affected = $stmt->rowCount(); - // Build inner query to remove permissions - if (!empty($removals)) { - foreach ($removals as $type => $permissionsToRemove) { - $bindKey = 'uid_' . $index; - $removeBindKeys[] = ':uid_' . $index; - $removeBindValues[$bindKey] = $document->getId(); + // Permissions logic + if (!empty($updates->getPermissions())) { + $removeQueries = []; + $removeBindValues = []; - $tenantQuery = ''; - if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant'; - } + $addQuery = ''; + $addBindValues = []; - $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; - $removeBindKeys[] = ':' . $bindKey; - $removeBindValues[$bindKey] = $permissionsToRemove[$i]; - - return ':' . $bindKey; - }, \array_keys($permissionsToRemove))) . - ") - )"; - - if ($type !== \array_key_last($removals)) { - $removeQuery .= ' OR '; - } - } + /* @var $document Document */ + foreach ($documents as $index => $document) { + // Permissions logic + $sql = " + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "; - if ($index !== \array_key_last($batch)) { - $removeQuery .= ' OR '; - } - } + if ($this->sharedTables) { + $sql .= ' AND _tenant = :_tenant'; + } - // Get added Permissions - $additions = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); - if (!empty($diff)) { - $additions[$type] = $diff; - } - } + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); - // Build inner query to add permissions - if (!empty($additions)) { - foreach ($additions as $type => $permissionsToAdd) { - foreach ($permissionsToAdd as $i => $permission) { - $bindKey = 'uid_' . $index; - $addBindValues[$bindKey] = $document->getId(); + $permissionsStmt = $this->getPDO()->prepare($sql); + $permissionsStmt->bindValue(':_uid', $document->getId()); - $bindKey = 'add_' . $type . '_' . $index . '_' . $i; - $addBindValues[$bindKey] = $permission; + if ($this->sharedTables) { + $permissionsStmt->bindValue(':_tenant', $this->tenant); + } - $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey}"; + $permissionsStmt->execute(); + $permissions = $permissionsStmt->fetchAll(); - if ($this->sharedTables) { - $addQuery .= ", :_tenant)"; - } else { - $addQuery .= ")"; - } + $initial = []; + foreach (Database::PERMISSIONS as $type) { + $initial[$type] = []; + } - if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { - $addQuery .= ', '; - } - } - } - if ($index !== \array_key_last($batch)) { - $addQuery .= ', '; + $permissions = \array_reduce($permissions, function (array $carry, array $item) { + $carry[$item['_type']][] = $item['_permission']; + return $carry; + }, $initial); + + // Get removed Permissions + $removals = []; + foreach (Database::PERMISSIONS as $type) { + $diff = array_diff($permissions[$type], $updates->getPermissionsByType($type)); + if (!empty($diff)) { + $removals[$type] = $diff; + } + } + + // Build inner query to remove permissions + if (!empty($removals)) { + foreach ($removals as $type => $permissionsToRemove) { + $bindKey = 'uid_' . $index; + $removeBindKeys[] = ':uid_' . $index; + $removeBindValues[$bindKey] = $document->getId(); + + $tenantQuery = ''; + if ($this->sharedTables) { + $tenantQuery = ' AND _tenant = :_tenant'; } + + $removeQueries[] = "( + _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; + $removeBindKeys[] = ':' . $bindKey; + $removeBindValues[$bindKey] = $permissionsToRemove[$i]; + + return ':' . $bindKey; + }, \array_keys($permissionsToRemove))) . + ") + )"; } } - $updateClause = ''; - for ($i = 0; $i < \count($columns); $i++) { - $column = $columns[$i]; - if (!empty($updateClause)) { - $updateClause .= ', '; + // Get added Permissions + $additions = []; + foreach (Database::PERMISSIONS as $type) { + $diff = \array_diff($updates->getPermissionsByType($type), $permissions[$type]); + if (!empty($diff)) { + $additions[$type] = $diff; } - $updateClause .= "{$column} = VALUES({$column})"; } - $stmt = $this->getPDO()->prepare(" - INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ") - VALUES " . \implode(', ', $batchKeys) . " - ON DUPLICATE KEY UPDATE $updateClause - "); + // Build inner query to add permissions + if (!empty($additions)) { + foreach ($additions as $type => $permissionsToAdd) { + foreach ($permissionsToAdd as $i => $permission) { + $bindKey = 'uid_' . $index; + $addBindValues[$bindKey] = $document->getId(); - foreach ($bindValues as $key => $value) { - $stmt->bindValue($key, $value, $this->getPDOType($value)); - } + $bindKey = 'add_' . $type . '_' . $index . '_' . $i; + $addBindValues[$bindKey] = $permission; - $stmt->execute(); + $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey}"; - if (!empty($removeQuery)) { - $stmtRemovePermissions = $this->getPDO()->prepare(" - DELETE - FROM {$this->getSQLTable($name . '_perms')} - WHERE ({$removeQuery}) - "); + if ($this->sharedTables) { + $addQuery .= ", :_tenant)"; + } else { + $addQuery .= ")"; + } - foreach ($removeBindValues as $key => $value) { - $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); + if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { + $addQuery .= ', '; + } + } } - if ($this->sharedTables) { - $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); + if ($index !== \array_key_last($documents)) { + $addQuery .= ', '; } - $stmtRemovePermissions->execute(); } + } - if (!empty($addQuery)) { - $sqlAddPermissions = " - INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission` - "; + if (!empty($removeQueries)) { + $removeQuery = \implode(' OR ', $removeQueries); - if ($this->sharedTables) { - $sqlAddPermissions .= ', `_tenant`)'; - } else { - $sqlAddPermissions .= ')'; - } + $stmtRemovePermissions = $this->getPDO()->prepare(" + DELETE + FROM {$this->getSQLTable($name . '_perms')} + WHERE ({$removeQuery}) + "); - $sqlAddPermissions .= " VALUES {$addQuery}"; + foreach ($removeBindValues as $key => $value) { + $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); + } - $stmtAddPermissions = $this->getPDO()->prepare($sqlAddPermissions); + if ($this->sharedTables) { + $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); + } + $stmtRemovePermissions->execute(); + } - foreach ($addBindValues as $key => $value) { - $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); - } + if (!empty($addQuery)) { + $sqlAddPermissions = " + INSERT INTO {$this->getSQLTable($name . '_perms')} (`_document`, `_type`, `_permission` + "; - if ($this->sharedTables) { - $stmtAddPermissions->bindValue(':_tenant', $this->tenant); - } + if ($this->sharedTables) { + $sqlAddPermissions .= ', `_tenant`)'; + } else { + $sqlAddPermissions .= ')'; + } - $stmtAddPermissions->execute(); + $sqlAddPermissions .= " VALUES {$addQuery}"; + + $stmtAddPermissions = $this->getPDO()->prepare($sqlAddPermissions); + + foreach ($addBindValues as $key => $value) { + $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); } - } - } catch (\Throwable $e) { - if ($e instanceof PDOException) { - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage(), previous: $e); + + if ($this->sharedTables) { + $stmtAddPermissions->bindValue(':_tenant', $this->tenant); } - } - throw $e; + $stmtAddPermissions->execute(); + } } - return $documents; + return $affected; } /** @@ -1690,11 +1689,12 @@ public function deleteDocument(string $collection, string $id): bool * @param array $orderTypes * @param array $cursor * @param string $cursorDirection + * @param string $forPermission * @return array * @throws DatabaseException * @throws TimeoutException */ - public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array + public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array { $name = $this->filter($collection); $roles = Authorization::getRoles(); @@ -1784,7 +1784,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if (Authorization::$status) { - $where[] = $this->getSQLPermissionsCondition($name, $roles); + $where[] = $this->getSQLPermissionsCondition($name, $roles, $forPermission); } if ($this->sharedTables) { diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index f17cec26b..61c0d7bb7 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -821,35 +821,46 @@ public function updateDocument(string $collection, string $id, Document $documen } /** - * Update Documents in batches + * Update documents + * + * Updates all documents which match the given query. * * @param string $collection + * @param Document $updates * @param array $documents - * @param int $batchSize * - * @return array + * @return int * - * @throws Duplicate + * @throws DatabaseException */ - public function updateDocuments(string $collection, array $documents, int $batchSize): array + public function updateDocuments(string $collection, Document $updates, array $documents): int { $name = $this->getNamespace() . '_' . $this->filter($collection); - foreach ($documents as $index => $document) { - $document = $document->getArrayCopy(); - $document = $this->replaceChars('$', '_', $document); - $document = $this->timeToMongo($document); + $queries = [ + Query::equal('$id', array_map(fn ($document) => $document->getId(), $documents)) + ]; - $filters = []; - $filters['_uid'] = $document['_uid']; - if ($this->sharedTables) { - $filters['_tenant'] = (string)$this->getTenant(); - } + $filters = $this->buildFilters($queries); + if ($this->sharedTables) { + $filters['_tenant'] = (string)$this->getTenant(); + } + + $record = $updates->getArrayCopy(); + $record = $this->replaceChars('$', '_', $record); + $record = $this->timeToMongo($record); - $this->client->update($name, $filters, $document); + $updateQuery = [ + '$set' => $record, + ]; + + try { + $this->client->update($name, $filters, $updateQuery, multi: true); + } catch (MongoException $e) { + throw new Duplicate($e->getMessage()); } - return $documents; + return 1; } /** @@ -954,12 +965,13 @@ public function updateAttribute(string $collection, string $id, string $type, in * @param array $orderTypes * @param array $cursor * @param string $cursorDirection + * @param string $forPermission * * @return array * @throws Exception * @throws Timeout */ - public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array + public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array { $name = $this->getNamespace() . '_' . $this->filter($collection); $queries = array_map(fn ($query) => clone $query, $queries); @@ -971,9 +983,9 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } // permissions - if (Authorization::$status) { // skip if authorization is disabled + if (Authorization::$status) { $roles = \implode('|', Authorization::getRoles()); - $filters['_permissions']['$in'] = [new Regex("read\(\".*(?:{$roles}).*\"\)", 'i')]; + $filters['_permissions']['$in'] = [new Regex("{$forPermission}\(\".*(?:{$roles}).*\"\)", 'i')]; } $options = []; @@ -1164,8 +1176,13 @@ private function timeToDocument(array $record): array */ private function timeToMongo(array $record): array { - $record['_createdAt'] = $this->toMongoDatetime($record['_createdAt']); - $record['_updatedAt'] = $this->toMongoDatetime($record['_updatedAt']); + if (isset($record['_createdAt'])) { + $record['_createdAt'] = $this->toMongoDatetime($record['_createdAt']); + } + + if (isset($record['_updatedAt'])) { + $record['_updatedAt'] = $this->toMongoDatetime($record['_updatedAt']); + } return $record; } @@ -1681,6 +1698,16 @@ public function getSupportForAttributeResizing(): bool return false; } + /** + * Are batch operations supported? + * + * @return bool + */ + public function getSupportForBatchOperations(): bool + { + return false; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 2b9007000..0082f4cde 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1206,252 +1206,258 @@ public function updateDocument(string $collection, string $id, Document $documen } /** - * Update Documents in batches + * Update documents + * + * Updates all documents which match the given query. * * @param string $collection + * @param Document $updates * @param array $documents - * @param int $batchSize * - * @return array + * @return int * - * @throws Duplicate + * @throws DatabaseException */ - public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array + public function updateDocuments(string $collection, Document $updates, array $documents): int { - if (empty($documents)) { - return $documents; - } + $attributes = $updates->getAttributes(); - try { - $name = $this->filter($collection); - $batches = \array_chunk($documents, max(1, $batchSize)); + if (!empty($updates->getUpdatedAt())) { + $attributes['_updatedAt'] = $updates->getUpdatedAt(); + } - foreach ($batches as $batch) { - $bindIndex = 0; - $batchKeys = []; - $bindValues = []; + if (!empty($updates->getPermissions())) { + $attributes['_permissions'] = json_encode($updates->getPermissions()); + } - $removeQuery = ''; - $removeBindValues = []; + if (empty($attributes)) { + return 0; + } - $addQuery = ''; - $addBindValues = []; + $name = $this->filter($collection); - foreach ($batch as $index => $document) { - $attributes = $document->getAttributes(); - $attributes['_uid'] = $document->getId(); - $attributes['_createdAt'] = $document->getCreatedAt(); - $attributes['_updatedAt'] = $document->getUpdatedAt(); - $attributes['_permissions'] = json_encode($document->getPermissions()); + $columns = ''; - if ($this->sharedTables) { - $attributes['_tenant'] = $this->tenant; - } + $where = []; - $columns = \array_map(function ($attribute) { - return '"' . $this->filter($attribute) . '"'; - }, \array_keys($attributes)); + $ids = \array_map(fn ($document) => $document->getId(), $documents); + $where[] = "_uid IN (" . \implode(', ', \array_map(fn ($index) => ":_id_{$index}", \array_keys($ids))) . ")"; - $bindKeys = []; + if ($this->sharedTables) { + $where[] = "_tenant = :_tenant"; + } - foreach ($attributes as $value) { - if (\is_array($value)) { - $value = json_encode($value); - } - $value = (is_bool($value)) ? (int)$value : $value; - $bindKey = 'key_' . $bindIndex; - $bindKeys[] = ':' . $bindKey; - $bindValues[$bindKey] = $value; - $bindIndex++; - } + $sqlWhere = 'WHERE ' . implode(' AND ', $where); - $batchKeys[] = '(' . implode(', ', $bindKeys) . ')'; + $bindIndex = 0; + foreach ($attributes as $attribute => $value) { + $column = $this->filter($attribute); + $bindKey = 'key_' . $bindIndex; + $columns .= "\"{$column}\"" . '=:' . $bindKey; - // Permissions logic - $sql = " - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} - WHERE _document = :_uid - "; + if ($attribute !== \array_key_last($attributes)) { + $columns .= ','; + } - if ($this->sharedTables) { - $sql .= ' AND _tenant = :_tenant'; - } + $bindIndex++; + } - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); + $sql = " + UPDATE {$this->getSQLTable($name)} + SET {$columns} + {$sqlWhere} + "; - $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $document->getId()); + $sql = $this->trigger(Database::EVENT_DOCUMENTS_UPDATE, $sql); + $stmt = $this->getPDO()->prepare($sql); - if ($this->sharedTables) { - $permissionsStmt->bindValue(':_tenant', $this->tenant); - } + if ($this->sharedTables) { + $stmt->bindValue(':_tenant', $this->tenant); + } - $permissionsStmt->execute(); - $permissions = $permissionsStmt->fetchAll(); + foreach ($ids as $id => $value) { + $stmt->bindValue(":_id_{$id}", $value); + } - $initial = []; - foreach (Database::PERMISSIONS as $type) { - $initial[$type] = []; - } + $attributeIndex = 0; + foreach ($attributes as $attribute => $value) { + if (is_array($value)) { + $value = json_encode($value); + } - $permissions = \array_reduce($permissions, function (array $carry, array $item) { - $carry[$item['_type']][] = $item['_permission']; - return $carry; - }, $initial); + $bindKey = 'key_' . $attributeIndex; + $value = (is_bool($value)) ? (int)$value : $value; + $stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value)); + $attributeIndex++; + } - // Get removed Permissions - $removals = []; - foreach (Database::PERMISSIONS as $type) { - $diff = array_diff($permissions[$type], $document->getPermissionsByType($type)); - if (!empty($diff)) { - $removals[$type] = $diff; - } - } + $stmt->execute(); + $affected = $stmt->rowCount(); - // Build inner query to remove permissions - if (!empty($removals)) { - foreach ($removals as $type => $permissionsToRemove) { - $bindKey = 'uid_' . $index; - $removeBindKeys[] = ':uid_' . $index; - $removeBindValues[$bindKey] = $document->getId(); + // Permissions logic + if (!empty($updates->getPermissions())) { + $removeQueries = []; + $removeBindValues = []; - $tenantQuery = ''; - if ($this->sharedTables) { - $tenantQuery = ' AND _tenant = :_tenant'; - } + $addQuery = ''; + $addBindValues = []; - $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; - $removeBindKeys[] = ':' . $bindKey; - $removeBindValues[$bindKey] = $permissionsToRemove[$i]; - - return ':' . $bindKey; - }, \array_keys($permissionsToRemove))) . - ") - )"; - - if ($type !== \array_key_last($removals)) { - $removeQuery .= ' OR '; - } - } + /* @var $document Document */ + foreach ($documents as $index => $document) { + // Permissions logic + $sql = " + SELECT _type, _permission + FROM {$this->getSQLTable($name . '_perms')} + WHERE _document = :_uid + "; - if ($index !== \array_key_last($batch)) { - $removeQuery .= ' OR '; - } - } + if ($this->sharedTables) { + $sql .= ' AND _tenant = :_tenant'; + } - // Get added Permissions - $additions = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); - if (!empty($diff)) { - $additions[$type] = $diff; - } - } + $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); - // Build inner query to add permissions - if (!empty($additions)) { - foreach ($additions as $type => $permissionsToAdd) { - foreach ($permissionsToAdd as $i => $permission) { - $bindKey = 'uid_' . $index; - $addBindValues[$bindKey] = $document->getId(); + $permissionsStmt = $this->getPDO()->prepare($sql); + $permissionsStmt->bindValue(':_uid', $document->getId()); - $bindKey = 'add_' . $type . '_' . $index . '_' . $i; - $addBindValues[$bindKey] = $permission; + if ($this->sharedTables) { + $permissionsStmt->bindValue(':_tenant', $this->tenant); + } - $tenantQuery = $this->sharedTables ? ', :_tenant' : ''; + $permissionsStmt->execute(); + $permissions = $permissionsStmt->fetchAll(); - $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey} {$tenantQuery})"; + $initial = []; + foreach (Database::PERMISSIONS as $type) { + $initial[$type] = []; + } - if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { - $addQuery .= ', '; - } - } - } - if ($index !== \array_key_last($batch)) { - $addQuery .= ', '; - } + $permissions = \array_reduce($permissions, function (array $carry, array $item) { + $carry[$item['_type']][] = $item['_permission']; + return $carry; + }, $initial); + + // Get removed Permissions + $removals = []; + foreach (Database::PERMISSIONS as $type) { + $diff = array_diff($permissions[$type], $updates->getPermissionsByType($type)); + if (!empty($diff)) { + $removals[$type] = $diff; } } - $updateClause = ''; - for ($i = 0; $i < \count($columns); $i++) { - $column = $columns[$i]; - if (!empty($updateClause)) { - $updateClause .= ', '; + // Build inner query to remove permissions + if (!empty($removals)) { + foreach ($removals as $type => $permissionsToRemove) { + $bindKey = 'uid_' . $index; + $removeBindKeys[] = ':uid_' . $index; + $removeBindValues[$bindKey] = $document->getId(); + + $tenantQuery = ''; + if ($this->sharedTables) { + $tenantQuery = ' AND _tenant = :_tenant'; + } + + $removeQueries[] = "( + _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; + $removeBindKeys[] = ':' . $bindKey; + $removeBindValues[$bindKey] = $permissionsToRemove[$i]; + + return ':' . $bindKey; + }, \array_keys($permissionsToRemove))) . + ") + )"; } - $updateClause .= "{$column} = excluded.{$column}"; } - $sql = " - INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ") - VALUES " . \implode(', ', $batchKeys) . " - "; - - if ($this->sharedTables) { - $sql .= "ON CONFLICT (_tenant, LOWER(_uid)) DO UPDATE SET $updateClause"; - } else { - $sql .= "ON CONFLICT (LOWER(_uid)) DO UPDATE SET $updateClause"; + // Get added Permissions + $additions = []; + foreach (Database::PERMISSIONS as $type) { + $diff = \array_diff($updates->getPermissionsByType($type), $permissions[$type]); + if (!empty($diff)) { + $additions[$type] = $diff; + } } - $stmt = $this->getPDO()->prepare($sql); + // Build inner query to add permissions + if (!empty($additions)) { + foreach ($additions as $type => $permissionsToAdd) { + foreach ($permissionsToAdd as $i => $permission) { + $bindKey = 'uid_' . $index; + $addBindValues[$bindKey] = $document->getId(); - foreach ($bindValues as $key => $value) { - $stmt->bindValue($key, $value, $this->getPDOType($value)); - } + $bindKey = 'add_' . $type . '_' . $index . '_' . $i; + $addBindValues[$bindKey] = $permission; - $stmt->execute(); + $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey}"; - if (!empty($removeQuery)) { - $stmtRemovePermissions = $this->getPDO()->prepare(" - DELETE - FROM {$this->getSQLTable($name . '_perms')} - WHERE ({$removeQuery}) - "); + if ($this->sharedTables) { + $addQuery .= ", :_tenant)"; + } else { + $addQuery .= ")"; + } - foreach ($removeBindValues as $key => $value) { - $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); + if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { + $addQuery .= ', '; + } + } } - if ($this->sharedTables) { - $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); + if ($index !== \array_key_last($documents)) { + $addQuery .= ', '; } + } + } - $stmtRemovePermissions->execute(); + if (!empty($removeQueries)) { + $removeQuery = \implode(' OR ', $removeQueries); + + $stmtRemovePermissions = $this->getPDO()->prepare(" + DELETE + FROM {$this->getSQLTable($name . '_perms')} + WHERE ({$removeQuery}) + "); + + foreach ($removeBindValues as $key => $value) { + $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); } + if ($this->sharedTables) { + $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); + } + $stmtRemovePermissions->execute(); + } - if (!empty($addQuery)) { - $tenantQuery = $this->sharedTables ? ', _tenant' : ''; + if (!empty($addQuery)) { + $sqlAddPermissions = " + INSERT INTO {$this->getSQLTable($name . '_perms')} (\"_document\", \"_type\", \"_permission\" + "; - $stmtAddPermissions = $this->getPDO()->prepare(" - INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission {$tenantQuery}) - VALUES {$addQuery} - "); + if ($this->sharedTables) { + $sqlAddPermissions .= ', "_tenant")'; + } else { + $sqlAddPermissions .= ')'; + } - foreach ($addBindValues as $key => $value) { - $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); - } + $sqlAddPermissions .= " VALUES {$addQuery}"; - if ($this->sharedTables) { - $stmtAddPermissions->bindValue(':_tenant', $this->tenant); - } + $stmtAddPermissions = $this->getPDO()->prepare($sqlAddPermissions); - $stmtAddPermissions->execute(); + foreach ($addBindValues as $key => $value) { + $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); } - } - return $documents; - } catch (PDOException $e) { + if ($this->sharedTables) { + $stmtAddPermissions->bindValue(':_tenant', $this->tenant); + } - throw match ($e->getCode()) { - 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), - default => $e, - }; + $stmtAddPermissions->execute(); + } } + + return $affected; } /** @@ -1583,13 +1589,14 @@ public function deleteDocument(string $collection, string $id): bool * @param array $orderTypes * @param array $cursor * @param string $cursorDirection + * @param string $forPermission * * @return array * @throws Exception * @throws PDOException * @throws Timeout */ - public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array + public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array { $name = $this->filter($collection); $roles = Authorization::getRoles(); @@ -1677,7 +1684,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } if (Authorization::$status) { - $where[] = $this->getSQLPermissionsCondition($name, $roles); + $where[] = $this->getSQLPermissionsCondition($name, $roles, $forPermission); } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index e73515a57..b06492f3f 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -361,6 +361,16 @@ public function getSupportForAttributeResizing(): bool return true; } + /** + * Are batch operations supported? + * + * @return bool + */ + public function getSupportForBatchOperations(): bool + { + return true; + } + /** * Get current attribute count from collection document * @@ -968,8 +978,12 @@ protected function getSQLIndexType(string $type): string * @return string * @throws Exception */ - protected function getSQLPermissionsCondition(string $collection, array $roles): string + protected function getSQLPermissionsCondition(string $collection, array $roles, string $type = Database::PERMISSION_READ): string { + if (!in_array($type, Database::PERMISSIONS)) { + throw new DatabaseException('Unknown permission type: ' . $type); + } + $roles = array_map(fn (string $role) => $this->getPDO()->quote($role), $roles); $tenantQuery = ''; @@ -981,7 +995,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): SELECT _document FROM {$this->getSQLTable($collection . '_perms')} WHERE _permission IN (" . implode(', ', $roles) . ") - AND _type = 'read' + AND _type = '{$type}' {$tenantQuery} )"; } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index bbe2bdfde..d88c21595 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -787,259 +787,7 @@ public function updateDocument(string $collection, string $id, Document $documen return $document; } - /** - * Update Documents in batches - * - * @param string $collection - * @param array $documents - * @param int $batchSize - * - * @return array - * - * @throws Duplicate - */ - public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array - { - if (empty($documents)) { - return $documents; - } - - try { - $name = $this->filter($collection); - $batches = \array_chunk($documents, max(1, $batchSize)); - - foreach ($batches as $batch) { - $bindIndex = 0; - $batchKeys = []; - $bindValues = []; - - $removeQuery = ''; - $removeBindValues = []; - - $addQuery = ''; - $addBindValues = []; - - foreach ($batch as $index => $document) { - $attributes = $document->getAttributes(); - $attributes['_uid'] = $document->getId(); - $attributes['_createdAt'] = $document->getCreatedAt(); - $attributes['_updatedAt'] = $document->getUpdatedAt(); - $attributes['_permissions'] = json_encode($document->getPermissions()); - - if ($this->sharedTables) { - $attributes['_tenant'] = $this->tenant; - } - - $columns = \array_map(function ($attribute) { - return "`" . $this->filter($attribute) . "`"; - }, \array_keys($attributes)); - - $bindKeys = []; - - foreach ($attributes as $value) { - if (\is_array($value)) { - $value = json_encode($value); - } - $value = (is_bool($value)) ? (int)$value : $value; - $bindKey = 'key_' . $bindIndex; - $bindKeys[] = ':' . $bindKey; - $bindValues[$bindKey] = $value; - $bindIndex++; - } - - $batchKeys[] = '(' . implode(', ', $bindKeys) . ')'; - - // Permissions logic - $sql = " - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} - WHERE _document = :_uid - "; - - if ($this->sharedTables) { - $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->sharedTables) { - $permissionsStmt->bindValue(':_tenant', $this->tenant); - } - - $permissionsStmt->execute(); - $permissions = $permissionsStmt->fetchAll(); - - $initial = []; - foreach (Database::PERMISSIONS as $type) { - $initial[$type] = []; - } - - $permissions = \array_reduce($permissions, function (array $carry, array $item) { - $carry[$item['_type']][] = $item['_permission']; - return $carry; - }, $initial); - - // Get removed Permissions - $removals = []; - foreach (Database::PERMISSIONS as $type) { - $diff = array_diff($permissions[$type], $document->getPermissionsByType($type)); - if (!empty($diff)) { - $removals[$type] = $diff; - } - } - - // Build inner query to remove permissions - if (!empty($removals)) { - foreach ($removals as $type => $permissionsToRemove) { - $bindKey = 'uid_' . $index; - $removeBindKeys[] = ':uid_' . $index; - $removeBindValues[$bindKey] = $document->getId(); - - $tenantQuery = ''; - if ($this->sharedTables) { - $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; - $removeBindKeys[] = ':' . $bindKey; - $removeBindValues[$bindKey] = $permissionsToRemove[$i]; - - return ':' . $bindKey; - }, \array_keys($permissionsToRemove))) . - ") - )"; - - if ($type !== \array_key_last($removals)) { - $removeQuery .= ' OR '; - } - } - - if ($index !== \array_key_last($batch)) { - $removeQuery .= ' OR '; - } - } - - // Get added Permissions - $additions = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); - if (!empty($diff)) { - $additions[$type] = $diff; - } - } - - // Build inner query to add permissions - if (!empty($additions)) { - foreach ($additions as $type => $permissionsToAdd) { - foreach ($permissionsToAdd as $i => $permission) { - $bindKey = 'uid_' . $index; - $addBindValues[$bindKey] = $document->getId(); - - $bindKey = 'add_' . $type . '_' . $index . '_' . $i; - $addBindValues[$bindKey] = $permission; - - $tenantQuery = $this->sharedTables ? ', :_tenant' : ''; - - $addQuery .= "(:uid_{$index}, '{$type}', :{$bindKey} {$tenantQuery})"; - - if ($i !== \array_key_last($permissionsToAdd) || $type !== \array_key_last($additions)) { - $addQuery .= ', '; - } - } - } - if ($index !== \array_key_last($batch)) { - $addQuery .= ', '; - } - } - } - - $updateClause = ''; - for ($i = 0; $i < \count($columns); $i++) { - $column = $columns[$i]; - if (!empty($updateClause)) { - $updateClause .= ', '; - } - $updateClause .= "{$column} = excluded.{$column}"; - } - - $sql = " - INSERT INTO {$this->getSQLTable($name)} (" . \implode(", ", $columns) . ") - VALUES " . \implode(', ', $batchKeys) . " - - "; - - if ($this->sharedTables) { - $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)); - } - - $stmt->execute(); - - if (!empty($removeQuery)) { - $stmtRemovePermissions = $this->getPDO()->prepare(" - DELETE - FROM {$this->getSQLTable($name . '_perms')} - WHERE ({$removeQuery}) - "); - - foreach ($removeBindValues as $key => $value) { - $stmtRemovePermissions->bindValue($key, $value, $this->getPDOType($value)); - } - - if ($this->sharedTables) { - $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); - } - - $stmtRemovePermissions->execute(); - } - - if (!empty($addQuery)) { - $tenantQuery = $this->sharedTables ? ', _tenant' : ''; - - $stmtAddPermissions = $this->getPDO()->prepare(" - INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission {$tenantQuery}) - VALUES {$addQuery} - "); - - foreach ($addBindValues as $key => $value) { - $stmtAddPermissions->bindValue($key, $value, $this->getPDOType($value)); - } - if ($this->sharedTables) { - $stmtAddPermissions->bindValue(':_tenant', $this->tenant); - } - - $stmtAddPermissions->execute(); - } - } - - return $documents; - } catch (PDOException $e) { - throw match ($e->getCode()) { - 1062, 23000 => new Duplicate('Duplicated document: ' . $e->getMessage()), - default => $e, - }; - } - } /** * Is schemas supported? @@ -1187,14 +935,14 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr * @return string * @throws Exception */ - protected function getSQLPermissionsCondition(string $collection, array $roles): string + protected function getSQLPermissionsCondition(string $collection, array $roles, string $type = Database::PERMISSION_READ): string { $roles = array_map(fn (string $role) => $this->getPDO()->quote($role), $roles); return "table_main._uid IN ( SELECT distinct(_document) FROM `{$this->getNamespace()}_{$collection}_perms` WHERE _permission IN (" . implode(', ', $roles) . ") - AND _type = 'read' + AND _type = '{$type}' )"; } diff --git a/src/Database/Database.php b/src/Database/Database.php index c032bd499..cbd099e61 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -19,6 +19,7 @@ use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Index as IndexValidator; +use Utopia\Database\Validator\PartialStructure; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Queries\Document as DocumentValidator; use Utopia\Database\Validator\Queries\Documents as DocumentsValidator; @@ -3900,84 +3901,117 @@ public function updateDocument(string $collection, string $id, Document $documen } /** - * Update Documents in a batch + * Update documents + * + * Updates all documents which match the given query. * * @param string $collection - * @param array $documents + * @param Document $updates + * @param array $queries * @param int $batchSize * - * @return array + * @return int * * @throws AuthorizationException - * @throws Exception - * @throws StructureException + * @throws DatabaseException */ - public function updateDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE): array + public function updateDocuments(string $collection, Document $updates, array $queries = [], int $batchSize = self::INSERT_BATCH_SIZE): int { if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } - if (empty($documents)) { - return []; + if ($updates->isEmpty()) { + return 0; } + $queries = Query::groupByType($queries)['filters']; $collection = $this->silent(fn () => $this->getCollection($collection)); - $documents = $this->withTransaction(function () use ($collection, $documents, $batchSize) { - $time = DateTime::now(); + unset($updates['$id']); + unset($updates['$createdAt']); + unset($updates['$tenant']); - foreach ($documents as $key => $document) { - if (!$document->getId()) { - throw new DatabaseException('Must define $id attribute for each document'); - } + if (!$this->preserveDates) { + $updates['$updatedAt'] = DateTime::now(); + } - $updatedAt = $document->getUpdatedAt(); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); - $document = $this->encode($collection, $document); - - $old = Authorization::skip(fn () => $this->silent( - fn () => $this->getDocument( - $collection->getId(), - $document->getId(), - forUpdate: true - ) - )); + $updates = $this->encode($collection, $updates); - $validator = new Authorization(self::PERMISSION_UPDATE); - if ( - $collection->getId() !== self::METADATA - && !$validator->isValid($old->getUpdate()) - ) { - throw new AuthorizationException($validator->getDescription()); - } + // Check new document structure + $validator = new PartialStructure($collection); + if (!$validator->isValid($updates)) { + throw new StructureException($validator->getDescription()); + } + + $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates) { + $lastDocument = null; + $totalModified = 0; + $affectedDocumentIds = []; + + $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Structure($collection); - if (!$validator->isValid($document)) { - throw new StructureException($validator->getDescription()); + $authorization = new Authorization(self::PERMISSION_UPDATE); + $skipAuth = $authorization->isValid($collection->getUpdate()); + + if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { + throw new AuthorizationException($authorization->getDescription()); + } + + // Resolve and update relationships + while (true) { + $affectedDocuments = $this->find($collection->getId(), array_merge( + empty($lastDocument) ? [ + Query::limit($batchSize), + ] : [ + Query::limit($batchSize), + Query::cursorAfter($lastDocument), + ], + $queries, + ), forPermission: Database::PERMISSION_UPDATE); + + if (empty($affectedDocuments)) { + break; } - if ($this->resolveRelationships) { - $documents[$key] = $this->silent(fn () => $this->updateDocumentRelationships($collection, $old, $document)); + foreach ($affectedDocuments as $document) { + if ($this->resolveRelationships) { + $newDocument = array_merge($document->getArrayCopy(), $updates->getArrayCopy()); + + $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, new Document($newDocument))); + } + + $affectedDocumentIds[] = $document->getId(); } - } - return $this->adapter->updateDocuments($collection->getId(), $documents, $batchSize); - }); + $getResults = fn () => $this->adapter->updateDocuments( + $collection->getId(), + $updates, + $affectedDocuments + ); - foreach ($documents as $key => $document) { - if ($this->resolveRelationships) { - $document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document)); + $result = $skipAuth ? $authorization->skip($getResults) : $getResults(); + + $totalModified += $result; + + if (count($affectedDocuments) < $batchSize) { + break; + } else { + $lastDocument = end($affectedDocuments); + } } - $documents[$key] = $this->decode($collection, $document); + $this->trigger(self::EVENT_DOCUMENTS_UPDATE, $affectedDocumentIds); - $this->purgeCachedDocument($collection->getId(), $document->getId()); - } + foreach ($affectedDocumentIds as $id) { + $this->purgeRelatedDocuments($collection, $id); + $this->purgeCachedDocument($collection->getId(), $id); + } - $this->trigger(self::EVENT_DOCUMENTS_UPDATE, $documents); + return $totalModified; + }); - return $documents; + return $affected; } /** @@ -5061,13 +5095,14 @@ public function purgeCachedDocument(string $collectionId, string $id): bool * * @param string $collection * @param array $queries + * @param string $forPermission * * @return array * @throws DatabaseException * @throws QueryException * @throws TimeoutException */ - public function find(string $collection, array $queries = []): array + public function find(string $collection, array $queries = [], string $forPermission = Database::PERMISSION_READ): array { if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); @@ -5091,7 +5126,7 @@ public function find(string $collection, array $queries = []): array $authorization = new Authorization(self::PERMISSION_READ); $documentSecurity = $collection->getAttribute('documentSecurity', false); - $skipAuth = $authorization->isValid($collection->getRead()); + $skipAuth = $authorization->isValid($collection->getPermissionsByType($forPermission)); if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { throw new AuthorizationException($authorization->getDescription()); @@ -5178,7 +5213,8 @@ public function find(string $collection, array $queries = []): array $orderAttributes, $orderTypes, $cursor, - $cursorDirection ?? Database::CURSOR_AFTER + $cursorDirection ?? Database::CURSOR_AFTER, + $forPermission ); $results = $skipAuth ? Authorization::skip($getResults) : $getResults(); diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php new file mode 100644 index 000000000..1f52a451c --- /dev/null +++ b/src/Database/Validator/PartialStructure.php @@ -0,0 +1,50 @@ +message = 'Value must be an instance of Document'; + return false; + } + + if (empty($this->collection->getId()) || Database::METADATA !== $this->collection->getCollection()) { + $this->message = 'Collection not found'; + return false; + } + + $keys = []; + $structure = $document->getArrayCopy(); + $attributes = \array_merge($this->attributes, $this->collection->getAttribute('attributes', [])); + + foreach ($attributes as $attribute) { + $name = $attribute['$id'] ?? ''; + $keys[$name] = $attribute; + } + + if (!$this->checkForUnknownAttributes($structure, $keys)) { + return false; + } + + if (!$this->checkForInvalidAttributeValues($structure, $keys)) { + return false; + } + + return true; + } +} diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 991a32240..2e4cfde97 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -227,6 +227,32 @@ public function isValid($document): bool $structure = $document->getArrayCopy(); $attributes = \array_merge($this->attributes, $this->collection->getAttribute('attributes', [])); + if (!$this->checkForAllRequiredValues($structure, $attributes, $keys)) { + return false; + } + + if (!$this->checkForUnknownAttributes($structure, $keys)) { + return false; + } + + if (!$this->checkForInvalidAttributeValues($structure, $keys)) { + return false; + } + + return true; + } + + /** + * Check for all required values + * + * @param array $structure + * @param array $attributes + * @param array $keys + * + * @return bool + */ + protected function checkForAllRequiredValues(array $structure, array $attributes, array &$keys): bool + { foreach ($attributes as $key => $attribute) { // Check all required attributes are set $name = $attribute['$id'] ?? ''; $required = $attribute['required'] ?? false; @@ -239,12 +265,40 @@ public function isValid($document): bool } } + return true; + } + + /** + * Check for Unknown Attributes + * + * @param array $structure + * @param array $keys + * + * @return bool + */ + protected function checkForUnknownAttributes(array $structure, array $keys): bool + { foreach ($structure as $key => $value) { if (!array_key_exists($key, $keys)) { // Check no unknown attributes are set $this->message = 'Unknown attribute: "'.$key.'"'; return false; } + } + return true; + } + + /** + * Check for invalid attribute values + * + * @param array $structure + * @param array $keys + * + * @return bool + */ + protected function checkForInvalidAttributeValues(array $structure, array $keys): bool + { + foreach ($structure as $key => $value) { $attribute = $keys[$key] ?? []; $type = $attribute['type'] ?? ''; $array = $attribute['array'] ?? false; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 297ec9d7a..b0c62fce2 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -751,9 +751,18 @@ public function testPreserveDatesUpdate(): void $doc1 = static::getDatabase()->getDocument('preserve_update_dates', 'doc1'); $this->assertEquals($newDate, $doc1->getAttribute('$updatedAt')); - $doc2->setAttribute('$updatedAt', $newDate); - $doc3->setAttribute('$updatedAt', $newDate); - static::getDatabase()->updateDocuments('preserve_update_dates', [$doc2, $doc3], 2); + $this->getDatabase()->updateDocuments( + 'preserve_update_dates', + new Document([ + '$updatedAt' => $newDate + ]), + [ + Query::equal('$id', [ + $doc2->getId(), + $doc3->getId() + ]) + ] + ); $doc2 = static::getDatabase()->getDocument('preserve_update_dates', 'doc2'); $doc3 = static::getDatabase()->getDocument('preserve_update_dates', 'doc3'); @@ -2455,53 +2464,6 @@ public function testUpdateDocument(Document $document): Document return $document; } - /** - * @depends testCreateDocuments - * @param array $documents - */ - public function testUpdateDocuments(array $documents): void - { - $collection = 'testCreateDocuments'; - - foreach ($documents as $document) { - $document - ->setAttribute('string', 'text📝 updated') - ->setAttribute('integer', 6) - ->setAttribute('$permissions', [ - Permission::read(Role::users()), - Permission::create(Role::users()), - Permission::update(Role::users()), - Permission::delete(Role::users()), - ]); - } - - $documents = static::getDatabase()->updateDocuments( - $collection, - $documents, - \count($documents) - ); - - foreach ($documents as $document) { - $this->assertEquals('text📝 updated', $document->getAttribute('string')); - $this->assertEquals(6, $document->getAttribute('integer')); - } - - $documents = static::getDatabase()->find($collection, [ - Query::limit(\count($documents)) - ]); - - foreach ($documents as $document) { - $this->assertEquals('text📝 updated', $document->getAttribute('string')); - $this->assertEquals(6, $document->getAttribute('integer')); - $this->assertEquals([ - Permission::read(Role::users()), - Permission::create(Role::users()), - Permission::update(Role::users()), - Permission::delete(Role::users()), - ], $document->getAttribute('$permissions')); - } - } - /** * @depends testUpdateDocument */ @@ -15673,6 +15635,409 @@ public function testTransformations(): void $this->assertTrue($result->isEmpty()); } + public function testUpdateDocuments(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collection = 'testUpdateDocuments'; + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + + static::getDatabase()->createCollection($collection, attributes: [ + new Document([ + '$id' => ID::custom('string'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 100, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => ID::custom('integer'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 10000, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]), + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ], documentSecurity: false); + + for ($i = 0; $i < 10; $i++) { + static::getDatabase()->createDocument($collection, new Document([ + '$id' => 'doc' . $i, + 'string' => 'text📝 ' . $i, + 'integer' => $i + ])); + } + + // Test Update half of the documents + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'text📝 updated', + ]), [ + Query::greaterThanEqual('integer', 5), + ]); + + $this->assertEquals($affected, 5); + + $updatedDocuments = static::getDatabase()->find($collection, [ + Query::greaterThanEqual('integer', 5), + ]); + + $this->assertEquals(count($updatedDocuments), 5); + + foreach ($updatedDocuments as $document) { + $this->assertEquals('text📝 updated', $document->getAttribute('string')); + } + + $controlDocuments = static::getDatabase()->find($collection, [ + Query::lessThan('integer', 5), + ]); + + $this->assertEquals(count($controlDocuments), 5); + + foreach ($controlDocuments as $document) { + $this->assertNotEquals('text📝 updated', $document->getAttribute('string')); + } + + // Test Update all documents + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'text📝 updated all', + ])); + + $this->assertEquals(10, $affected); + + $updatedDocuments = static::getDatabase()->find($collection); + + $this->assertEquals(count($updatedDocuments), 10); + + foreach ($updatedDocuments as $document) { + $this->assertEquals('text📝 updated all', $document->getAttribute('string')); + } + + // Check collection level permissions + static::getDatabase()->updateCollection($collection, permissions: [ + Permission::read(Role::user('asd')), + Permission::create(Role::user('asd')), + Permission::update(Role::user('asd')), + Permission::delete(Role::user('asd')), + ], documentSecurity: false); + + try { + static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'text📝 updated all', + ])); + $this->fail('Failed to throw exception'); + } catch (AuthorizationException $e) { + $this->assertStringStartsWith('Missing "update" permission for role "user:asd".', $e->getMessage()); + } + + // Check document level permissions + static::getDatabase()->updateCollection($collection, permissions: [], documentSecurity: true); + + Authorization::skip(function () use ($collection) { + static::getDatabase()->updateDocument($collection, 'doc0', new Document([ + 'string' => 'text📝 updated all', + '$permissions' => [ + Permission::read(Role::user('asd')), + Permission::create(Role::user('asd')), + Permission::update(Role::user('asd')), + Permission::delete(Role::user('asd')), + ], + ])); + }); + + Authorization::setRole(Role::user('asd')->toString()); + + static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'permission text', + ])); + + $documents = static::getDatabase()->find($collection, [ + Query::equal('string', ['permission text']), + ]); + + $this->assertCount(1, $documents); + + Authorization::skip(function () use ($collection) { + $unmodifiedDocuments = static::getDatabase()->find($collection, [ + Query::equal('string', ['text📝 updated all']), + ]); + + $this->assertCount(9, $unmodifiedDocuments); + }); + + Authorization::skip(function () use ($collection) { + static::getDatabase()->updateDocuments($collection, new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ])); + }); + + // Test we can update more documents than batchSize + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'batchSize Test' + ]), batchSize: 2); + + $documents = static::getDatabase()->find($collection); + + $this->assertEquals(10, $affected); + + foreach ($documents as $document) { + $this->assertEquals('batchSize Test', $document->getAttribute('string')); + } + + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + } + + public function testUpdateDocumentsPermissions(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collection = 'testUpdateDocumentsPerms'; + + static::getDatabase()->createCollection($collection, attributes: [ + new Document([ + '$id' => ID::custom('string'), + 'type' => Database::VAR_STRING, + 'size' => 767, + 'required' => true, + ]) + ], permissions: [], documentSecurity: true); + + // Test we can bulk update permissions we have access to + Authorization::skip(function () use ($collection) { + for ($i = 0; $i < 10; $i++) { + static::getDatabase()->createDocument($collection, new Document([ + '$id' => 'doc' . $i, + 'string' => 'text📝 ' . $i, + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ], + ])); + } + + static::getDatabase()->createDocument($collection, new Document([ + '$id' => 'doc' . $i, + 'string' => 'text📝 ' . $i, + '$permissions' => [ + Permission::read(Role::user('user1')), + Permission::create(Role::user('user1')), + Permission::update(Role::user('user1')), + Permission::delete(Role::user('user1')) + ], + ])); + }); + + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + '$permissions' => [ + Permission::read(Role::user('user2')), + Permission::create(Role::user('user2')), + Permission::update(Role::user('user2')), + Permission::delete(Role::user('user2')) + ], + ])); + + $documents = Authorization::skip(function () use ($collection) { + return static::getDatabase()->find($collection); + }); + + $this->assertEquals(10, $affected); + $this->assertCount(11, $documents); + + $modifiedDocuments = array_filter($documents, function (Document $document) { + return $document->getAttribute('$permissions') == [ + Permission::read(Role::user('user2')), + Permission::create(Role::user('user2')), + Permission::update(Role::user('user2')), + Permission::delete(Role::user('user2')) + ]; + }); + + $this->assertCount(10, $modifiedDocuments); + + $unmodifiedDocuments = array_filter($documents, function (Document $document) { + return $document->getAttribute('$permissions') == [ + Permission::read(Role::user('user1')), + Permission::create(Role::user('user1')), + Permission::update(Role::user('user1')), + Permission::delete(Role::user('user1')) + ]; + }); + + $this->assertCount(1, $unmodifiedDocuments); + + Authorization::setRole(Role::user('user2')->toString()); + + // Test Bulk permission update with data + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + '$permissions' => [ + Permission::read(Role::user('user3')), + Permission::create(Role::user('user3')), + Permission::update(Role::user('user3')), + Permission::delete(Role::user('user3')) + ], + 'string' => 'text📝 updated', + ])); + + $this->assertEquals(10, $affected); + + $documents = Authorization::skip(function () use ($collection) { + return $this->getDatabase()->find($collection); + }); + + $this->assertCount(11, $documents); + + $modifiedDocuments = array_filter($documents, function (Document $document) { + return $document->getAttribute('$permissions') == [ + Permission::read(Role::user('user3')), + Permission::create(Role::user('user3')), + Permission::update(Role::user('user3')), + Permission::delete(Role::user('user3')) + ]; + }); + + foreach ($modifiedDocuments as $document) { + $this->assertEquals('text📝 updated', $document->getAttribute('string')); + } + } + + public function testUpdateDocumentsRelationships(): void + { + if (!$this->getDatabase()->getAdapter()->getSupportForBatchOperations() || !$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + + $this->getDatabase()->createCollection('testUpdateDocumentsRelationships1', attributes: [ + new Document([ + '$id' => ID::custom('string'), + 'type' => Database::VAR_STRING, + 'size' => 767, + 'required' => true, + ]) + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]); + + $this->getDatabase()->createCollection('testUpdateDocumentsRelationships2', attributes: [ + new Document([ + '$id' => ID::custom('string'), + 'type' => Database::VAR_STRING, + 'size' => 767, + 'required' => true, + ]) + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]); + + $this->getDatabase()->createRelationship( + collection: 'testUpdateDocumentsRelationships1', + relatedCollection: 'testUpdateDocumentsRelationships2', + type: Database::RELATION_ONE_TO_ONE, + twoWay: true, + ); + + $this->getDatabase()->createDocument('testUpdateDocumentsRelationships1', new Document([ + '$id' => 'doc1', + 'string' => 'text📝', + ])); + + $this->getDatabase()->createDocument('testUpdateDocumentsRelationships2', new Document([ + '$id' => 'doc1', + 'string' => 'text📝', + 'testUpdateDocumentsRelationships1' => 'doc1' + ])); + + $sisterDocument = $this->getDatabase()->getDocument('testUpdateDocumentsRelationships2', 'doc1'); + $this->assertNotNull($sisterDocument); + + $this->getDatabase()->updateDocuments('testUpdateDocumentsRelationships1', new Document([ + 'string' => 'text📝 updated', + ])); + + $document = $this->getDatabase()->findOne('testUpdateDocumentsRelationships1'); + + $this->assertNotFalse($document); + $this->assertEquals('text📝 updated', $document->getAttribute('string')); + + $sisterDocument = $this->getDatabase()->getDocument('testUpdateDocumentsRelationships2', 'doc1'); + $this->assertNotNull($sisterDocument); + + $relationalDocument = $sisterDocument->getAttribute('testUpdateDocumentsRelationships1'); + $this->assertEquals('text📝 updated', $relationalDocument->getAttribute('string')); + + // Check relationship value updating between each other. + $this->getDatabase()->deleteRelationship('testUpdateDocumentsRelationships1', 'testUpdateDocumentsRelationships2'); + + $this->getDatabase()->createRelationship( + collection: 'testUpdateDocumentsRelationships1', + relatedCollection: 'testUpdateDocumentsRelationships2', + type: Database::RELATION_ONE_TO_MANY, + twoWay: true, + ); + + for ($i = 2; $i < 11; $i++) { + $this->getDatabase()->createDocument('testUpdateDocumentsRelationships1', new Document([ + '$id' => 'doc' . $i, + 'string' => 'text📝', + ])); + + $this->getDatabase()->createDocument('testUpdateDocumentsRelationships2', new Document([ + '$id' => 'doc' . $i, + 'string' => 'text📝', + 'testUpdateDocumentsRelationships1' => 'doc' . $i + ])); + } + + $this->getDatabase()->updateDocuments('testUpdateDocumentsRelationships2', new Document([ + 'testUpdateDocumentsRelationships1' => null + ])); + + $this->getDatabase()->updateDocuments('testUpdateDocumentsRelationships2', new Document([ + 'testUpdateDocumentsRelationships1' => 'doc1' + ])); + + $documents = $this->getDatabase()->find('testUpdateDocumentsRelationships2'); + + foreach ($documents as $document) { + $this->assertEquals('doc1', $document->getAttribute('testUpdateDocumentsRelationships1')->getId()); + } + } + public function testEvents(): void { Authorization::skip(function () { From 616aa2f17283696249428271b463f05448fdba83 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 1 Nov 2024 20:53:14 +1300 Subject: [PATCH 104/256] Merge pull request #447 from utopia-php/feat-batch-delete Batch Deletes --- src/Database/Adapter.php | 10 + src/Database/Adapter/MariaDB.php | 69 ++++ src/Database/Adapter/Mongo.php | 37 ++ src/Database/Adapter/Postgres.php | 70 ++++ src/Database/Database.php | 86 +++- tests/e2e/Adapter/Base.php | 643 ++++++++++++++++++++++++++++++ 6 files changed, 914 insertions(+), 1 deletion(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index d174a8759..99f4add78 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -589,6 +589,16 @@ abstract public function updateDocuments(string $collection, Document $updates, */ abstract public function deleteDocument(string $collection, string $id): bool; + /** + * Delete Documents + * + * @param string $collection + * @param array $ids + * + * @return int + */ + abstract public function deleteDocuments(string $collection, array $ids): int; + /** * Find Documents * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3df5cdd2d..6fdd187bd 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1678,6 +1678,75 @@ public function deleteDocument(string $collection, string $id): bool return $deleted; } + /** + * Delete Documents + * + * @param string $collection + * @param array $ids + * + * @return int + */ + public function deleteDocuments(string $collection, array $ids): int + { + try { + $name = $this->filter($collection); + $where = []; + + if ($this->sharedTables) { + $where[] = "_tenant = :_tenant"; + } + + $where[] = "_uid IN (" . \implode(', ', \array_map(fn ($index) => ":_id_{$index}", \array_keys($ids))) . ")"; + + $sql = "DELETE FROM {$this->getSQLTable($name)} WHERE " . \implode(' AND ', $where); + + $sql = $this->trigger(Database::EVENT_DOCUMENTS_DELETE, $sql); + + $stmt = $this->getPDO()->prepare($sql); + + foreach ($ids as $id => $value) { + $stmt->bindValue(":_id_{$id}", $value); + } + + if ($this->sharedTables) { + $stmt->bindValue(':_tenant', $this->tenant); + } + + $sql = " + DELETE FROM {$this->getSQLTable($name . '_perms')} + WHERE _document IN (" . \implode(', ', \array_map(fn ($index) => ":_id_{$index}", \array_keys($ids))) . ") + "; + + if ($this->sharedTables) { + $sql .= ' AND _tenant = :_tenant'; + } + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + + $stmtPermissions = $this->getPDO()->prepare($sql); + + foreach ($ids as $id => $value) { + $stmtPermissions->bindValue(":_id_{$id}", $value); + } + + if ($this->sharedTables) { + $stmtPermissions->bindValue(':_tenant', $this->tenant); + } + + if (!$stmt->execute()) { + throw new DatabaseException('Failed to delete documents'); + } + + if (!$stmtPermissions->execute()) { + throw new DatabaseException('Failed to delete permissions'); + } + } catch (\Throwable $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + return $stmt->rowCount(); + } + /** * Find Documents * diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 61c0d7bb7..105fab4cb 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -931,6 +931,43 @@ public function deleteDocument(string $collection, string $id): bool return (!!$result); } + /** + * Delete Documents + * + * @param string $collection + * @param array $ids + * + * @return int + */ + public function deleteDocuments(string $collection, array $ids): int + { + $name = $this->getNamespace() . '_' . $this->filter($collection); + + $filters = $this->buildFilters([new Query(Query::TYPE_EQUAL, '_uid', $ids)]); + + if ($this->sharedTables) { + $filters['_tenant'] = (string)$this->getTenant(); + } + + $filters = $this->replaceInternalIdsKeys($filters, '$', '_', $this->operators); + $filters = $this->timeFilter($filters); + + $options = []; + + try { + $count = $this->client->delete( + collection: $name, + filters: $filters, + options: $options, + limit: 0 + ); + } catch (MongoException $e) { + $this->processException($e); + } + + return $count ?? 0; + } + /** * Update Attribute. * @param string $collection diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 0082f4cde..5fdc08e06 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1576,6 +1576,76 @@ public function deleteDocument(string $collection, string $id): bool return $deleted; } + + /** + * Delete Documents + * + * @param string $collection + * @param array $ids + * + * @return int + */ + public function deleteDocuments(string $collection, array $ids): int + { + try { + $name = $this->filter($collection); + $where = []; + + if ($this->sharedTables) { + $where[] = "_tenant = :_tenant"; + } + + $where[] = "_uid IN (" . \implode(', ', \array_map(fn ($index) => ":_id_{$index}", \array_keys($ids))) . ")"; + + $sql = "DELETE FROM {$this->getSQLTable($name)} WHERE " . \implode(' AND ', $where); + + $sql = $this->trigger(Database::EVENT_DOCUMENTS_DELETE, $sql); + + $stmt = $this->getPDO()->prepare($sql); + + foreach ($ids as $id => $value) { + $stmt->bindValue(":_id_{$id}", $value); + } + + if ($this->sharedTables) { + $stmt->bindValue(':_tenant', $this->tenant); + } + + $sql = " + DELETE FROM {$this->getSQLTable($name . '_perms')} + WHERE _document IN (" . \implode(', ', \array_map(fn ($id) => ":_id_{$id}", \array_keys($ids))) . ") + "; + + if ($this->sharedTables) { + $sql .= ' AND _tenant = :_tenant'; + } + + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + + $stmtPermissions = $this->getPDO()->prepare($sql); + + foreach ($ids as $id => $value) { + $stmtPermissions->bindValue(":_id_{$id}", $value); + } + + if ($this->sharedTables) { + $stmtPermissions->bindValue(':_tenant', $this->tenant); + } + + if (!$stmt->execute()) { + throw new DatabaseException('Failed to delete documents'); + } + + if (!$stmtPermissions->execute()) { + throw new DatabaseException('Failed to delete permissions'); + } + } catch (\Throwable $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + return $stmt->rowCount(); + } + /** * Find Documents * diff --git a/src/Database/Database.php b/src/Database/Database.php index cbd099e61..efde2151f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -114,6 +114,7 @@ class Database public const EVENT_DOCUMENT_FIND = 'document_find'; public const EVENT_DOCUMENT_CREATE = 'document_create'; public const EVENT_DOCUMENTS_CREATE = 'documents_create'; + public const EVENT_DOCUMENTS_DELETE = 'documents_delete'; public const EVENT_DOCUMENT_READ = 'document_read'; public const EVENT_DOCUMENT_UPDATE = 'document_update'; public const EVENT_DOCUMENTS_UPDATE = 'documents_update'; @@ -136,6 +137,7 @@ class Database public const EVENT_INDEX_DELETE = 'index_delete'; public const INSERT_BATCH_SIZE = 100; + public const DELETE_BATCH_SIZE = 100; protected Adapter $adapter; @@ -4976,7 +4978,7 @@ private function deleteCascade(Document $collection, Document $relatedCollection $this->deleteDocument( $relatedCollection->getId(), - $value->getId() + ($value instanceof Document) ? $value->getId() : $value ); \array_pop($this->relationshipDeleteStack); @@ -5051,6 +5053,88 @@ private function deleteCascade(Document $collection, Document $relatedCollection } } + /** + * Delete Documents + * + * Deletes all documents which match the given query, will respect the relationship's onDelete optin. + * + * @param string $collection + * @param array $queries + * @param int $batchSize + * + * @return int + * + * @throws AuthorizationException + * @throws DatabaseException + * @throws RestrictedException + */ + public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): int + { + if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { + throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); + } + + $queries = Query::groupByType($queries)['filters']; + $collection = $this->silent(fn () => $this->getCollection($collection)); + $affectedDocumentIds = []; + + $deleted = $this->withTransaction(function () use ($collection, $queries, $batchSize, $affectedDocumentIds) { + $lastDocument = null; + + $documentSecurity = $collection->getAttribute('documentSecurity', false); + $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_DELETE, $collection->getDelete())); + + if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { + throw new AuthorizationException($this->authorization->getDescription()); + } + + while (true) { + $affectedDocuments = $this->find($collection->getId(), array_merge( + empty($lastDocument) ? [ + Query::limit($batchSize), + ] : [ + Query::limit($batchSize), + Query::cursorAfter($lastDocument), + ], + $queries, + ), forPermission: Database::PERMISSION_DELETE); + + if (empty($affectedDocuments)) { + break; + } + + $affectedDocumentIds = array_merge($affectedDocumentIds, array_map(fn ($document) => $document->getId(), $affectedDocuments)); + + foreach ($affectedDocuments as $document) { + // Delete Relationships + if ($this->resolveRelationships) { + $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); + } + + $this->purgeRelatedDocuments($collection, $document->getId()); + $this->purgeCachedDocument($collection->getId(), $document->getId()); + } + + if (count($affectedDocuments) < $batchSize) { + break; + } else { + $lastDocument = end($affectedDocuments); + } + } + + if (empty($affectedDocumentIds)) { + return 0; + } + + $this->trigger(self::EVENT_DOCUMENTS_DELETE, $affectedDocumentIds); + + // Mass delete using adapter with query + return $this->adapter->deleteDocuments($collection->getId(), $affectedDocumentIds); + }); + + return $deleted; + } + /** * Cleans the all the collection's documents from the cache * And the all related cached documents. diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b0c62fce2..d603d27f9 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15635,6 +15635,649 @@ public function testTransformations(): void $this->assertTrue($result->isEmpty()); } + public function propegateBulkDocuments(bool $documentSecurity = false): void + { + for ($i = 0; $i < 10; $i++) { + static::getDatabase()->createDocument('bulk_delete', new Document( + array_merge([ + '$id' => 'doc' . $i, + 'text' => 'value' . $i, + 'integer' => $i + ], $documentSecurity ? [ + '$permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ], + ] : []) + )); + } + } + + public function testDeleteBulkDocuments(): void + { + static::getDatabase()->createCollection( + 'bulk_delete', + attributes: [ + new Document([ + '$id' => 'text', + 'type' => Database::VAR_STRING, + 'size' => 100, + 'required' => true, + ]), + new Document([ + '$id' => 'integer', + 'type' => Database::VAR_INTEGER, + 'size' => 10, + 'required' => true, + ]) + ], + documentSecurity: false, + permissions: [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()) + ] + ); + + $this->propegateBulkDocuments(); + + $docs = static::getDatabase()->find('bulk_delete'); + $this->assertCount(10, $docs); + + // TEST: Bulk Delete All Documents + $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); + $this->assertEquals(10, $deleted); + + $docs = static::getDatabase()->find('bulk_delete'); + $this->assertCount(0, $docs); + + // TEST: Bulk delete documents with queries. + $this->propegateBulkDocuments(); + + $deleted = static::getDatabase()->deleteDocuments('bulk_delete', [ + Query::greaterThanEqual('integer', 5) + ]); + $this->assertEquals(5, $deleted); + + $docs = static::getDatabase()->find('bulk_delete'); + $this->assertCount(5, $docs); + + // TEST (FAIL): Bulk delete all documents with invalid collection permission + static::getDatabase()->updateCollection('bulk_delete', [], false); + try { + static::getDatabase()->deleteDocuments('bulk_delete'); + $this->fail('Bulk deleted documents with invalid collection permission'); + } catch (\Utopia\Database\Exception\Authorization) { + } + + static::getDatabase()->updateCollection('bulk_delete', [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()) + ], false); + $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); + + $this->assertEquals(5, $deleted); + $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete'))); + + // TEST: Make sure we can't delete documents we don't have permissions for + static::getDatabase()->updateCollection('bulk_delete', [ + Permission::create(Role::any()), + ], true); + $this->propegateBulkDocuments(true); + + $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); + $this->assertEquals(0, $deleted); + + $documents = static::$authorization->skip(function () { + return static::getDatabase()->find('bulk_delete'); + }); + + $this->assertCount(10, $documents); + + static::getDatabase()->updateCollection('bulk_delete', [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()) + ], false); + static::getDatabase()->deleteDocuments('bulk_delete'); + $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete'))); + + // Teardown + static::getDatabase()->deleteCollection('bulk_delete'); + } + + public function testDeleteBulkDocumentsOneToOneRelationship(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + $this->getDatabase()->createCollection('bulk_delete_person_o2o'); + $this->getDatabase()->createCollection('bulk_delete_library_o2o'); + + $this->getDatabase()->createAttribute('bulk_delete_person_o2o', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_o2o', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_o2o', 'area', Database::VAR_STRING, 255, true); + + // Restrict + $this->getDatabase()->createRelationship( + collection: 'bulk_delete_person_o2o', + relatedCollection: 'bulk_delete_library_o2o', + type: Database::RELATION_ONE_TO_ONE, + onDelete: Database::RELATION_MUTATE_RESTRICT + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2o' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $library = $person1->getAttribute('bulk_delete_library_o2o'); + $this->assertEquals('library1', $library['$id']); + $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); + + // Delete person + try { + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->fail('Failed to throw exception'); + } catch (RestrictedException $e) { + $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); + } + + $this->getDatabase()->updateDocument('bulk_delete_person_o2o', 'person1', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2o' => null, + ])); + + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + + // NULL + $this->getDatabase()->updateRelationship( + collection: 'bulk_delete_person_o2o', + id: 'bulk_delete_library_o2o', + onDelete: Database::RELATION_MUTATE_SET_NULL + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2o' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $library = $person1->getAttribute('bulk_delete_library_o2o'); + $this->assertEquals('library1', $library['$id']); + $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); + + $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + $this->assertCount(1, $this->getDatabase()->find('bulk_delete_person_o2o')); + + $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $library = $person->getAttribute('bulk_delete_library_o2o'); + $this->assertNull($library); + + // NULL - Cleanup + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + + // Cascade + $this->getDatabase()->updateRelationship( + collection: 'bulk_delete_person_o2o', + id: 'bulk_delete_library_o2o', + onDelete: Database::RELATION_MUTATE_CASCADE + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2o' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $library = $person1->getAttribute('bulk_delete_library_o2o'); + $this->assertEquals('library1', $library['$id']); + $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); + + $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + $this->assertCount(1, $this->getDatabase()->find('bulk_delete_person_o2o')); + + $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $library = $person->getAttribute('bulk_delete_library_o2o'); + $this->assertEmpty($library); + $this->assertNotNull($library); + + // Test Bulk delete parent + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2o' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $library = $person1->getAttribute('bulk_delete_library_o2o'); + $this->assertEquals('library1', $library['$id']); + $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); + + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + } + + public function testDeleteBulkDocumentsOneToManyRelationship(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + $this->getDatabase()->createCollection('bulk_delete_person_o2m'); + $this->getDatabase()->createCollection('bulk_delete_library_o2m'); + + $this->getDatabase()->createAttribute('bulk_delete_person_o2m', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_o2m', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_o2m', 'area', Database::VAR_STRING, 255, true); + + // Restrict + $this->getDatabase()->createRelationship( + collection: 'bulk_delete_person_o2m', + relatedCollection: 'bulk_delete_library_o2m', + type: Database::RELATION_ONE_TO_MANY, + onDelete: Database::RELATION_MUTATE_RESTRICT + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2m' => [ + [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + [ + '$id' => 'library2', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 2', + 'area' => 'Area 2', + ], + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $libraries = $person1->getAttribute('bulk_delete_library_o2m'); + $this->assertCount(2, $libraries); + + // Delete person + try { + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2m'); + $this->fail('Failed to throw exception'); + } catch (RestrictedException $e) { + $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); + } + + // Restrict Cleanup + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2m')); + + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2m')); + + // NULL + $this->getDatabase()->updateRelationship( + collection: 'bulk_delete_person_o2m', + id: 'bulk_delete_library_o2m', + onDelete: Database::RELATION_MUTATE_SET_NULL + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2m' => [ + [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + [ + '$id' => 'library2', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 2', + 'area' => 'Area 2', + ], + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $libraries = $person1->getAttribute('bulk_delete_library_o2m'); + $this->assertCount(2, $libraries); + + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2m')); + + $person = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $libraries = $person->getAttribute('bulk_delete_library_o2m'); + $this->assertEmpty($libraries); + + // NULL - Cleanup + $this->getDatabase()->deleteDocuments('bulk_delete_person_o2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2m')); + + + // Cascade + $this->getDatabase()->updateRelationship( + collection: 'bulk_delete_person_o2m', + id: 'bulk_delete_library_o2m', + onDelete: Database::RELATION_MUTATE_CASCADE + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_o2m' => [ + [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + [ + '$id' => 'library2', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 2', + 'area' => 'Area 2', + ], + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $libraries = $person1->getAttribute('bulk_delete_library_o2m'); + $this->assertCount(2, $libraries); + + $this->getDatabase()->deleteDocuments('bulk_delete_library_o2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2m')); + + $person = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $libraries = $person->getAttribute('bulk_delete_library_o2m'); + $this->assertEmpty($libraries); + } + + public function testDeleteBulkDocumentsManyToManyRelationship(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + $this->getDatabase()->createCollection('bulk_delete_person_m2m'); + $this->getDatabase()->createCollection('bulk_delete_library_m2m'); + + $this->getDatabase()->createAttribute('bulk_delete_person_m2m', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_m2m', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_m2m', 'area', Database::VAR_STRING, 255, true); + + // Many-to-Many Relationship + $this->getDatabase()->createRelationship( + collection: 'bulk_delete_person_m2m', + relatedCollection: 'bulk_delete_library_m2m', + type: Database::RELATION_MANY_TO_MANY, + onDelete: Database::RELATION_MUTATE_RESTRICT + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_m2m', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_m2m' => [ + [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + [ + '$id' => 'library2', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 2', + 'area' => 'Area 2', + ], + ], + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_m2m', 'person1'); + $libraries = $person1->getAttribute('bulk_delete_library_m2m'); + $this->assertCount(2, $libraries); + + // Delete person + try { + $this->getDatabase()->deleteDocuments('bulk_delete_person_m2m'); + $this->fail('Failed to throw exception'); + } catch (RestrictedException $e) { + $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); + } + + // Restrict Cleanup + $this->getDatabase()->deleteRelationship('bulk_delete_person_m2m', 'bulk_delete_library_m2m'); + $this->getDatabase()->deleteDocuments('bulk_delete_library_m2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_m2m')); + + $this->getDatabase()->deleteDocuments('bulk_delete_person_m2m'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_m2m')); + } + + public function testDeleteBulkDocumentsManyToOneRelationship(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + $this->expectNotToPerformAssertions(); + return; + } + + $this->getDatabase()->createCollection('bulk_delete_person_m2o'); + $this->getDatabase()->createCollection('bulk_delete_library_m2o'); + + $this->getDatabase()->createAttribute('bulk_delete_person_m2o', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_m2o', 'name', Database::VAR_STRING, 255, true); + $this->getDatabase()->createAttribute('bulk_delete_library_m2o', 'area', Database::VAR_STRING, 255, true); + + // Many-to-One Relationship + $this->getDatabase()->createRelationship( + collection: 'bulk_delete_person_m2o', + relatedCollection: 'bulk_delete_library_m2o', + type: Database::RELATION_MANY_TO_ONE, + onDelete: Database::RELATION_MUTATE_RESTRICT + ); + + $person1 = $this->getDatabase()->createDocument('bulk_delete_person_m2o', new Document([ + '$id' => 'person1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 1', + 'bulk_delete_library_m2o' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Library 1', + 'area' => 'Area 1', + ], + ])); + + $person2 = $this->getDatabase()->createDocument('bulk_delete_person_m2o', new Document([ + '$id' => 'person2', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => 'Person 2', + 'bulk_delete_library_m2o' => [ + '$id' => 'library1', + ] + ])); + + $person1 = $this->getDatabase()->getDocument('bulk_delete_person_m2o', 'person1'); + $library = $person1->getAttribute('bulk_delete_library_m2o'); + $this->assertEquals('library1', $library['$id']); + + // Delete library + try { + $this->getDatabase()->deleteDocuments('bulk_delete_library_m2o'); + $this->fail('Failed to throw exception'); + } catch (RestrictedException $e) { + $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); + } + + $this->assertEquals(2, count($this->getDatabase()->find('bulk_delete_person_m2o'))); + + // Test delete people + $this->getDatabase()->deleteDocuments('bulk_delete_person_m2o'); + $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete_person_m2o'))); + + // Restrict Cleanup + $this->getDatabase()->deleteDocuments('bulk_delete_library_m2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_m2o')); + + $this->getDatabase()->deleteDocuments('bulk_delete_person_m2o'); + $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_m2o')); + } + public function testUpdateDocuments(): void { if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { From 84558a6bdfc870a98035b50b43841dae1418a19d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 1 Nov 2024 21:08:46 +1300 Subject: [PATCH 105/256] Fix tests --- src/Database/Database.php | 5 +++-- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index efde2151f..3fb8af6ce 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5082,10 +5082,11 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $lastDocument = null; $documentSecurity = $collection->getAttribute('documentSecurity', false); - $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_DELETE, $collection->getDelete())); + $authorization = new Authorization(self::PERMISSION_DELETE); + $skipAuth = $authorization->isValid($collection->getDelete()); if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { - throw new AuthorizationException($this->authorization->getDescription()); + throw new AuthorizationException($authorization->getDescription()); } while (true) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index d603d27f9..20c7930f3 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15729,7 +15729,7 @@ public function testDeleteBulkDocuments(): void $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); $this->assertEquals(0, $deleted); - $documents = static::$authorization->skip(function () { + $documents = Authorization::skip(function () { return static::getDatabase()->find('bulk_delete'); }); From 39550cb0e32cac45bf5ea1deb96569b837c028b1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 4 Nov 2024 15:02:23 +1300 Subject: [PATCH 106/256] Rollback in transaction --- src/Database/Adapter/SQL.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index b06492f3f..745d2e59a 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -34,6 +34,10 @@ public function startTransaction(): bool { try { if ($this->inTransaction === 0) { + if ($this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); + } + $result = $this->getPDO()->beginTransaction(); } else { $result = true; From afb9e96e9f9efc4617170e662f13ba7fe24b1e53 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 4 Nov 2024 18:46:17 +1300 Subject: [PATCH 107/256] Min/max date validation based on adapter limits --- composer.json | 2 +- src/Database/Adapter.php | 102 ++++++++++-------- src/Database/Adapter/MariaDB.php | 42 +++++--- src/Database/Adapter/Mongo.php | 59 ++++++----- src/Database/Adapter/Postgres.php | 82 +++++++------- src/Database/Adapter/SQLite.php | 22 ++-- src/Database/Database.php | 48 +++++++-- src/Database/Validator/Datetime.php | 59 +++++------ src/Database/Validator/Queries/Documents.php | 16 ++- src/Database/Validator/Query/Filter.php | 20 ++-- src/Database/Validator/Structure.php | 18 ++-- tests/e2e/Adapter/Base.php | 5 +- tests/unit/Validator/DateTimeTest.php | 106 ++++++++++++++----- 13 files changed, 362 insertions(+), 219 deletions(-) diff --git a/composer.json b/composer.json index 995ee1ef1..a1ca02e4b 100755 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "require": { "ext-pdo": "*", "ext-mbstring": "*", - "php": ">=8.0", + "php": ">=8.3", "utopia-php/framework": "0.33.*", "utopia-php/cache": "0.10.*", "utopia-php/mongo": "0.3.*" diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 99f4add78..f0f63b659 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -234,6 +234,35 @@ public function resetMetadata(): self return $this; } + /** + * Set a global timeout for database queries in milliseconds. + * + * This function allows you to set a maximum execution time for all database + * queries executed using the library, or a specific event specified by the + * event parameter. Once this timeout is set, any database query that takes + * longer than the specified time will be automatically terminated by the library, + * and an appropriate error or exception will be raised to handle the timeout condition. + * + * @param int $milliseconds The timeout value in milliseconds for database queries. + * @param string $event The event the timeout should fire fore + * @return void + * + * @throws Exception The provided timeout value must be greater than or equal to 0. + */ + abstract public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void; + + /** + * Clears a global timeout for database queries. + * + * @param string $event + * @return void + */ + public function clearTimeout(string $event): void + { + // Clear existing callback + $this->before($event, 'timeout', null); + } + /** * Start a new transaction. * @@ -395,6 +424,14 @@ abstract public function createCollection(string $name, array $attributes = [], */ abstract public function deleteCollection(string $id): bool; + /** + * Analyze a collection updating it's metadata on the database engine + * + * @param string $collection + * @return bool + */ + abstract public function analyzeCollection(string $collection): bool; + /** * Create Attribute * @@ -687,6 +724,28 @@ abstract public function getLimitForAttributes(): int; */ abstract public function getLimitForIndexes(): int; + /** + * @return int + */ + abstract public function getMaxIndexLength(): int; + + /** + * Get the minimum supported DateTime value + * + * @return \DateTime + */ + abstract public function getMinDateTime(): \DateTime; + + /** + * Get the maximum supported DateTime value + * + * @return \DateTime + */ + public function getMaxDateTime(): \DateTime + { + return new \DateTime('9999-12-31 23:59:59'); + } + /** * Is schemas supported? * @@ -921,47 +980,4 @@ public function escapeWildcards(string $value): string * @throws Exception */ abstract public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool; - - /** - * @return int - */ - abstract public function getMaxIndexLength(): int; - - - /** - * Set a global timeout for database queries in milliseconds. - * - * This function allows you to set a maximum execution time for all database - * queries executed using the library, or a specific event specified by the - * event parameter. Once this timeout is set, any database query that takes - * longer than the specified time will be automatically terminated by the library, - * and an appropriate error or exception will be raised to handle the timeout condition. - * - * @param int $milliseconds The timeout value in milliseconds for database queries. - * @param string $event The event the timeout should fire fore - * @return void - * - * @throws Exception The provided timeout value must be greater than or equal to 0. - */ - abstract public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void; - - /** - * Clears a global timeout for database queries. - * - * @param string $event - * @return void - */ - public function clearTimeout(string $event): void - { - // Clear existing callback - $this->before($event, 'timeout', null); - } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - abstract public function analyzeCollection(string $collection): bool; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ade82a750..0e72eee4e 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -313,6 +313,22 @@ public function deleteCollection(string $id): bool ->execute(); } + /** + * Analyze a collection updating it's metadata on the database engine + * + * @param string $collection + * @return bool + */ + public function analyzeCollection(string $collection): bool + { + $name = $this->filter($collection); + + $sql = "ANALYZE TABLE {$this->getSQLTable($name)}"; + + $stmt = $this->getPDO()->prepare($sql); + return $stmt->execute(); + } + /** * Create Attribute * @@ -2285,6 +2301,16 @@ protected function getPDOType(mixed $value): int }; } + public function getMinDateTime(): \DateTime + { + return new \DateTime('1000-01-01 00:00:00'); + } + + public function getMaxDateTime(): \DateTime + { + return new \DateTime('9999-12-31 23:59:59'); + } + /** * Is fulltext Wildcard index supported? * @@ -2372,20 +2398,4 @@ protected function processException(PDOException $e): void throw $e; } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - public function analyzeCollection(string $collection): bool - { - $name = $this->filter($collection); - - $sql = "ANALYZE TABLE {$this->getSQLTable($name)}"; - - $stmt = $this->getPDO()->prepare($sql); - return $stmt->execute(); - } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 105fab4cb..433068396 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -57,6 +57,22 @@ public function __construct(Client $client) $this->client->connect(); } + public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void + { + if (!$this->getSupportForTimeouts()) { + return; + } + + $this->timeout = $milliseconds; + } + + public function clearTimeout(string $event): void + { + parent::clearTimeout($event); + + $this->timeout = null; + } + public function startTransaction(): bool { return true; @@ -320,6 +336,17 @@ public function deleteCollection(string $id): bool return (!!$this->getClient()->dropCollection($id)); } + /** + * Analyze a collection updating it's metadata on the database engine + * + * @param string $collection + * @return bool + */ + public function analyzeCollection(string $collection): bool + { + return false; + } + /** * Create Attribute * @@ -1640,6 +1667,11 @@ public function getLimitForIndexes(): int return 64; } + public function getMinDateTime(): \DateTime + { + return new \DateTime('-9999-01-01 00:00:00'); + } + /** * Is schemas supported? * @@ -1894,31 +1926,4 @@ public function getMaxIndexLength(): int { return 0; } - - public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void - { - if (!$this->getSupportForTimeouts()) { - return; - } - - $this->timeout = $milliseconds; - } - - public function clearTimeout(string $event): void - { - parent::clearTimeout($event); - - $this->timeout = null; - } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - public function analyzeCollection(string $collection): bool - { - return false; - } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 4f6d6a3dc..983e112dd 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -26,6 +26,30 @@ class Postgres extends SQL * 4. Full-text search is different - to_tsvector() and to_tsquery() */ + /** + * Returns Max Execution Time + * @param int $milliseconds + * @param string $event + * @return void + * @throws DatabaseException + */ + public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void + { + if (!$this->getSupportForTimeouts()) { + return; + } + if ($milliseconds <= 0) { + throw new DatabaseException('Timeout must be greater than 0'); + } + $this->before($event, 'timeout', function ($sql) use ($milliseconds) { + return " + SET statement_timeout = {$milliseconds}; + {$sql}; + SET statement_timeout = 0; + "; + }); + } + /** * Create Database * @@ -256,6 +280,17 @@ public function deleteCollection(string $id): bool ->execute(); } + /** + * Analyze a collection updating it's metadata on the database engine + * + * @param string $collection + * @return bool + */ + public function analyzeCollection(string $collection): bool + { + return false; + } + /** * Create Attribute * @@ -2233,12 +2268,16 @@ protected function decodeArray(array $value): string return '{' . implode(",", $value) . '}'; } + public function getMinDateTime(): \DateTime + { + return new \DateTime('-4713-01-01 00:00:00'); + } + /** * Is fulltext Wildcard index supported? * * @return bool */ - // TODO: Fix full-text search logic for postgres and MariaDB public function getSupportForFulltextWildcardIndex(): bool { return false; @@ -2265,27 +2304,11 @@ public function getSupportForJSONOverlaps(): bool } /** - * Returns Max Execution Time - * @param int $milliseconds - * @param string $event - * @return void - * @throws DatabaseException + * @return string */ - public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): void + public function getLikeOperator(): string { - if (!$this->getSupportForTimeouts()) { - return; - } - if ($milliseconds <= 0) { - throw new DatabaseException('Timeout must be greater than 0'); - } - $this->before($event, 'timeout', function ($sql) use ($milliseconds) { - return " - SET statement_timeout = {$milliseconds}; - {$sql}; - SET statement_timeout = 0; - "; - }); + return 'ILIKE'; } /** @@ -2314,23 +2337,4 @@ protected function processException(PDOException $e): void throw $e; } - - /** - * @return string - */ - public function getLikeOperator(): string - { - return 'ILIKE'; - } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - public function analyzeCollection(string $collection): bool - { - return false; - } } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 8303ed720..5d1945598 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -267,6 +267,17 @@ public function deleteCollection(string $id): bool return true; } + /** + * Analyze a collection updating it's metadata on the database engine + * + * @param string $collection + * @return bool + */ + public function analyzeCollection(string $collection): bool + { + return false; + } + /** * Update Attribute * @@ -1140,15 +1151,4 @@ protected function processException(PDOException $e): void throw $e; } - - /** - * Analyze a collection updating it's metadata on the database engine - * - * @param string $collection - * @return bool - */ - public function analyzeCollection(string $collection): bool - { - return false; - } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 7fdad32f9..ffbc61175 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3287,7 +3287,11 @@ public function createDocument(string $collection, Document $document): Document } } - $structure = new Structure($collection); + $structure = new Structure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$structure->isValid($document)) { throw new StructureException($structure->getDescription()); } @@ -3349,7 +3353,11 @@ public function createDocuments(string $collection, array $documents, int $batch $document = $this->encode($collection, $document); - $validator = new Structure($collection); + $validator = new Structure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($document)) { throw new StructureException($validator->getDescription()); } @@ -3876,7 +3884,11 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->encode($collection, $document); - $structureValidator = new Structure($collection); + $structureValidator = new Structure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($structureValidator->getDescription()); } @@ -3942,7 +3954,11 @@ public function updateDocuments(string $collection, Document $updates, array $qu $updates = $this->encode($collection, $updates); // Check new document structure - $validator = new PartialStructure($collection); + $validator = new PartialStructure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($updates)) { throw new StructureException($validator->getDescription()); } @@ -5204,7 +5220,13 @@ public function find(string $collection, array $queries = [], string $forPermiss $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes, maxValuesCount: $this->maxQueryValues); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5384,7 +5406,13 @@ public function count(string $collection, array $queries = [], ?int $max = null) $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes, maxValuesCount: $this->maxQueryValues); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5430,7 +5458,13 @@ public function sum(string $collection, string $attribute, array $queries = [], $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes, maxValuesCount: $this->maxQueryValues); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } diff --git a/src/Database/Validator/Datetime.php b/src/Database/Validator/Datetime.php index 812669d7a..0d5c887f6 100644 --- a/src/Database/Validator/Datetime.php +++ b/src/Database/Validator/Datetime.php @@ -13,33 +13,18 @@ class Datetime extends Validator public const PRECISION_ANY = 'any'; /** - * @var string + * @throws \Exception */ - protected string $precision = self::PRECISION_ANY; - - /** - * @var bool - */ - protected bool $requireDateInFuture = false; - - - /** - * @var int minimum offset from now in seconds - */ - protected int $offset = 0; - - /** - * @param int $offset minimum offset from now in seconds - */ - public function __construct(bool $requireDateInFuture = false, string $precision = self::PRECISION_ANY, int $offset = 0) - { + public function __construct( + private readonly \DateTime $min = new \DateTime('0000-01-01'), + private readonly \DateTime $max = new \DateTime('9999-12-31'), + private readonly bool $requireDateInFuture = false, + private readonly string $precision = self::PRECISION_ANY, + private readonly int $offset = 0, + ) { if ($offset < 0) { - throw new \Exception('Offset must be a positive number.'); + throw new \Exception('Offset must be a positive integer.'); } - - $this->requireDateInFuture = $requireDateInFuture; - $this->offset = $offset; - $this->precision = $precision; } /** @@ -51,16 +36,19 @@ public function getDescription(): string $message = 'Value must be valid date'; if ($this->offset > 0) { - $message .= " at least " . $this->offset . " seconds in future"; + $message .= " at least " . $this->offset . " seconds in the future and"; } elseif ($this->requireDateInFuture) { - $message .= " in future"; + $message .= " in the future and"; } if ($this->precision !== self::PRECISION_ANY) { $message .= " with " . $this->precision . " precision"; } - $message .= '.'; + $min = $this->min->format('Y-m-d H:i:s'); + $max = $this->max->format('Y-m-d H:i:s'); + + $message .= " between {$min} and {$max}."; return $message; } @@ -114,13 +102,24 @@ public function isValid($value): bool return false; } } - } catch (\Exception $e) { + } catch (\Exception) { return false; } - [$year] = explode('-', $value); + // Custom year validation to account for PHP allowing year overflow + $matches = []; + if (preg_match('/(?min->format('Y'); + $maxYear = (int)$this->max->format('Y'); + if ($year < $minYear || $year > $maxYear) { + return false; + } + } else { + return false; + } - if ((int)$year > 9999) { + if ($date < $this->min || $date > $this->max) { return false; } diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index d308f874d..abce8694f 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -22,8 +22,13 @@ class Documents extends IndexedQueries * @param array $indexes * @throws Exception */ - public function __construct(array $attributes, array $indexes, int $maxValuesCount = 100) - { + public function __construct( + array $attributes, + array $indexes, + int $maxValuesCount = 100, + \DateTime $minAllowedDate = new \DateTime('0000-01-01'), + \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + ) { $attributes[] = new Document([ '$id' => '$id', 'key' => '$id', @@ -53,7 +58,12 @@ public function __construct(array $attributes, array $indexes, int $maxValuesCou new Limit(), new Offset(), new Cursor(), - new Filter($attributes, $maxValuesCount), + new Filter( + $attributes, + $maxValuesCount, + $minAllowedDate, + $maxAllowedDate, + ), new Order($attributes), new Select($attributes), ]; diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 1fb50f9fc..9c810efcc 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -18,19 +18,21 @@ class Filter extends Base */ protected array $schema = []; - private int $maxValuesCount; - /** * @param array $attributes * @param int $maxValuesCount + * @param \DateTime $minAllowedDate + * @param \DateTime $maxAllowedDate */ - public function __construct(array $attributes = [], int $maxValuesCount = 100) - { + public function __construct( + array $attributes = [], + private readonly int $maxValuesCount = 100, + private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), + private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + ) { foreach ($attributes as $attribute) { $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); } - - $this->maxValuesCount = $maxValuesCount; } /** @@ -93,7 +95,6 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $attributeType = $attributeSchema['type']; foreach ($values as $value) { - $validator = null; switch ($attributeType) { @@ -114,7 +115,10 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s break; case Database::VAR_DATETIME: - $validator = new DatetimeValidator(); + $validator = new DatetimeValidator( + min: $this->minAllowedDate, + max: $this->maxAllowedDate + ); break; case Database::VAR_RELATIONSHIP: diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 2e4cfde97..7b2e8b6f5 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -17,11 +17,6 @@ class Structure extends Validator { - /** - * @var Document - */ - protected Document $collection; - /** * @var array> */ @@ -106,9 +101,11 @@ class Structure extends Validator * Structure constructor. * */ - public function __construct(Document $collection) - { - $this->collection = $collection; + public function __construct( + protected readonly Document $collection, + private readonly \DateTime $minAllowedDate = new \DateTime('0000-01-01'), + private readonly \DateTime $maxAllowedDate = new \DateTime('9999-12-31'), + ) { } /** @@ -342,7 +339,10 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) break; case Database::VAR_DATETIME: - $validators[] = new DatetimeValidator(); + $validators[] = new DatetimeValidator( + min: $this->minAllowedDate, + max: $this->maxAllowedDate + ); break; default: diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 20c7930f3..6930d8d93 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6138,7 +6138,10 @@ public function testCreateDatetime(): void $this->assertGreaterThan('2020-08-16T19:30:08.363+00:00', $doc->getUpdatedAt()); $document = static::getDatabase()->getDocument('datetime', 'id1234'); - $dateValidator = new DatetimeValidator(); + + $min = static::getDatabase()->getAdapter()->getMinDateTime(); + $max = static::getDatabase()->getAdapter()->getMaxDateTime(); + $dateValidator = new DatetimeValidator($min, $max); $this->assertEquals(null, $document->getAttribute('date2')); $this->assertEquals(true, $dateValidator->isValid($document->getAttribute('date'))); $this->assertEquals(false, $dateValidator->isValid($document->getAttribute('date2'))); diff --git a/tests/unit/Validator/DateTimeTest.php b/tests/unit/Validator/DateTimeTest.php index d8ababb6f..106080c29 100644 --- a/tests/unit/Validator/DateTimeTest.php +++ b/tests/unit/Validator/DateTimeTest.php @@ -8,6 +8,19 @@ class DateTimeTest extends TestCase { + private \DateTime $minAllowed; + private \DateTime $maxAllowed; + private string $minString = '0000-01-01 00:00:00'; + private string $maxString = '9999-12-31 23:59:59'; + + public function __construct() + { + parent::__construct(); + + $this->minAllowed = new \DateTime($this->minString); + $this->maxAllowed = new \DateTime($this->maxString); + } + public function setUp(): void { } @@ -18,7 +31,7 @@ public function tearDown(): void public function testCreateDatetime(): void { - $dateValidator = new DatetimeValidator(); + $dateValidator = new DatetimeValidator($this->minAllowed, $this->maxAllowed); $this->assertGreaterThan(DateTime::addSeconds(new \DateTime(), -3), DateTime::now()); $this->assertEquals(true, $dateValidator->isValid("2022-12-04")); @@ -29,7 +42,7 @@ public function testCreateDatetime(): void $now = DateTime::now(); $this->assertEquals(23, strlen($now)); $this->assertGreaterThan('2020-1-1 11:31:52.680', $now); - $this->assertEquals('Value must be valid date.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); $date = '2022-07-02 18:31:52.680'; $dateObject = new \DateTime($date); @@ -51,63 +64,100 @@ public function testCreateDatetime(): void * Test for Failure */ $this->assertEquals(false, $dateValidator->isValid("2022-13-04 11:31:52.680")); + $this->assertEquals(false, $dateValidator->isValid("-0001-13-04 00:00:00")); + $this->assertEquals(false, $dateValidator->isValid("0000-00-00 00:00:00")); + $this->assertEquals(false, $dateValidator->isValid("10000-01-01 00:00:00")); } public function testPastDateValidation(): void { - $dateValidator = new DatetimeValidator(requireDateInFuture: true); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + requireDateInFuture: true, + ); + $this->assertEquals(false, $dateValidator->isValid(DateTime::addSeconds(new \DateTime(), -3))); $this->assertEquals(true, $dateValidator->isValid(DateTime::addSeconds(new \DateTime(), 5))); - $this->assertEquals('Value must be valid date in future.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date in the future and between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); + + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + requireDateInFuture: false + ); - $dateValidator = new DatetimeValidator(requireDateInFuture: false); $this->assertEquals(true, $dateValidator->isValid(DateTime::addSeconds(new \DateTime(), -3))); $this->assertEquals(true, $dateValidator->isValid(DateTime::addSeconds(new \DateTime(), 5))); - $this->assertEquals('Value must be valid date.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); } public function testDatePrecision(): void { - $dateValidator = new DatetimeValidator(precision: DatetimeValidator::PRECISION_SECONDS); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + precision: DatetimeValidator::PRECISION_SECONDS + ); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.255')))); $this->assertEquals(true, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.000')))); $this->assertEquals(true, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26')))); - $this->assertEquals('Value must be valid date with seconds precision.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date with seconds precision between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); - $dateValidator = new DatetimeValidator(precision: DatetimeValidator::PRECISION_MINUTES); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + precision: DatetimeValidator::PRECISION_MINUTES + ); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.255')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.000')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26')))); $this->assertEquals(true, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:00')))); - $this->assertEquals('Value must be valid date with minutes precision.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date with minutes precision between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); - $dateValidator = new DatetimeValidator(precision: DatetimeValidator::PRECISION_HOURS); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + precision: DatetimeValidator::PRECISION_HOURS + ); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.255')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.000')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:00')))); $this->assertEquals(true, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:00:00')))); - $this->assertEquals('Value must be valid date with hours precision.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date with hours precision between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); - $dateValidator = new DatetimeValidator(precision: DatetimeValidator::PRECISION_DAYS); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + precision: DatetimeValidator::PRECISION_DAYS + ); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.255')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.000')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:00')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:00:00')))); $this->assertEquals(true, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 00:00:00')))); - $this->assertEquals('Value must be valid date with days precision.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date with days precision between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); - $dateValidator = new DatetimeValidator(true, DatetimeValidator::PRECISION_MINUTES); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + precision: DatetimeValidator::PRECISION_MINUTES + ); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2019-04-08 19:36:26.255')))); $this->assertEquals(false, $dateValidator->isValid(DateTime::format(new \DateTime('2100-04-08 19:36:26.255')))); $this->assertEquals(true, $dateValidator->isValid(DateTime::format(new \DateTime('2100-04-08 19:36:00')))); - $this->assertEquals('Value must be valid date in future with minutes precision.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date with minutes precision between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); } public function testOffset(): void { - $dateValidator = new DatetimeValidator(offset: 60); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + offset: 60 + ); $time = (new \DateTime()); $this->assertEquals(false, $dateValidator->isValid(DateTime::format($time))); @@ -115,22 +165,30 @@ public function testOffset(): void $this->assertEquals(false, $dateValidator->isValid(DateTime::format($time))); $time = $time->add(new \DateInterval('PT20S')); $this->assertEquals(true, $dateValidator->isValid(DateTime::format($time))); - $this->assertEquals('Value must be valid date at least 60 seconds in future.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date at least 60 seconds in the future and between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); - $dateValidator = new DatetimeValidator(requireDateInFuture: true, offset: 60); + $dateValidator = new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + requireDateInFuture: true, + offset: 60 + ); $time = (new \DateTime()); $time = $time->add(new \DateInterval('PT50S')); $time = $time->add(new \DateInterval('PT20S')); $this->assertEquals(true, $dateValidator->isValid(DateTime::format($time))); - $this->assertEquals('Value must be valid date at least 60 seconds in future.', $dateValidator->getDescription()); + $this->assertEquals("Value must be valid date at least 60 seconds in the future and between {$this->minString} and {$this->maxString}.", $dateValidator->getDescription()); - $threwException = false; try { - $dateValidator = new DatetimeValidator(offset: -60); + new DatetimeValidator( + $this->minAllowed, + $this->maxAllowed, + offset: -60 + ); + $this->fail('Failed to throw exception when offset is negative.'); } catch (\Exception $e) { - $threwException = true; + $this->assertEquals('Offset must be a positive integer.', $e->getMessage()); } - $this->assertTrue($threwException); } } From a98a71b6d47f7375bd112ee51e845de0ed329337 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 4 Nov 2024 13:52:45 +0200 Subject: [PATCH 108/256] Validate date --- composer.lock | 4 ++-- src/Database/Database.php | 4 ++-- src/Database/Validator/Datetime.php | 2 +- tests/e2e/Adapter/Base.php | 9 +++++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 49c2e3ddf..cc1b58ef6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8b51c1a58da391a80c01cd5b691b739", + "content-hash": "0fffc3b6680e12db596e0fb8dd971606", "packages": [ { "name": "jean85/pretty-package-versions", @@ -2591,7 +2591,7 @@ "platform": { "ext-pdo": "*", "ext-mbstring": "*", - "php": ">=8.0" + "php": ">=8.3" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/src/Database/Database.php b/src/Database/Database.php index ffbc61175..10a79b3cb 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -406,11 +406,11 @@ function (mixed $value) { self::addFilter( 'datetime', /** - * @param string|null $value + * @param mixed $value * @return string|null * @throws Exception */ - function (?string $value) { + function (mixed $value) { if (is_null($value)) { return null; } diff --git a/src/Database/Validator/Datetime.php b/src/Database/Validator/Datetime.php index 0d5c887f6..7950b1e07 100644 --- a/src/Database/Validator/Datetime.php +++ b/src/Database/Validator/Datetime.php @@ -60,7 +60,7 @@ public function getDescription(): string */ public function isValid($value): bool { - if (empty($value)) { + if (empty($value) || ! is_string($value)) { return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6930d8d93..6fde24f1f 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6119,6 +6119,15 @@ public function testCreateDatetime(): void $this->assertEquals(true, static::getDatabase()->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); $this->assertEquals(true, static::getDatabase()->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); + try { + static::getDatabase()->createDocument('datetime', new Document([ + 'date' => ['2020-01-01'], // array + ])); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + $doc = static::getDatabase()->createDocument('datetime', new Document([ '$id' => ID::custom('id1234'), '$permissions' => [ From 0b24787365dc3dc2c22c442a4d705e33dafa1037 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 4 Nov 2024 14:00:54 +0200 Subject: [PATCH 109/256] lint --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 10a79b3cb..356a6e439 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -412,7 +412,7 @@ function (mixed $value) { */ function (mixed $value) { if (is_null($value)) { - return null; + return; } try { $value = new \DateTime($value); From 184ba52bb067d502a5c33f0992fad172beec4a38 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 4 Nov 2024 14:11:21 +0200 Subject: [PATCH 110/256] revert lock --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index cc1b58ef6..49c2e3ddf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0fffc3b6680e12db596e0fb8dd971606", + "content-hash": "d8b51c1a58da391a80c01cd5b691b739", "packages": [ { "name": "jean85/pretty-package-versions", @@ -2591,7 +2591,7 @@ "platform": { "ext-pdo": "*", "ext-mbstring": "*", - "php": ">=8.3" + "php": ">=8.0" }, "platform-dev": [], "plugin-api-version": "2.6.0" From 4a367eb98c29c8be73188b5f72b213d37f87f539 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 4 Nov 2024 14:17:44 +0200 Subject: [PATCH 111/256] return null --- src/Database/Database.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 356a6e439..ea148e221 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -407,12 +407,11 @@ function (mixed $value) { 'datetime', /** * @param mixed $value - * @return string|null - * @throws Exception + * @return mixed */ function (mixed $value) { if (is_null($value)) { - return; + return null; } try { $value = new \DateTime($value); From 2952a8f947aedecd72bffbb0894645dfb1d51b1e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 4 Nov 2024 14:24:14 +0200 Subject: [PATCH 112/256] lint --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index ea148e221..13645e36f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -411,7 +411,7 @@ function (mixed $value) { */ function (mixed $value) { if (is_null($value)) { - return null; + return; } try { $value = new \DateTime($value); From bbfbf23a20b57080b27950accf0b53606ed92314 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Tue, 5 Nov 2024 12:21:15 +0900 Subject: [PATCH 113/256] Don't throw on deleteAttribute if column doesn't exist --- src/Database/Adapter/MariaDB.php | 10 +++++++++- src/Database/Adapter/Postgres.php | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0e72eee4e..40984e012 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -419,9 +419,17 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql); - return $this->getPDO() + try { + return $this->getPDO() ->prepare($sql) ->execute(); + } catch (PDOException $e) { + if ($e->getCode() === "42000" && $e->errorInfo[1] === 1091) { + return true; + } + + throw $e; + } } /** diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 983e112dd..14986dfba 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -346,9 +346,17 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $sql = $this->trigger(Database::EVENT_ATTRIBUTE_DELETE, $sql); - return $this->getPDO() + try { + return $this->getPDO() ->prepare($sql) ->execute(); + } catch (PDOException $e) { + if ($e->getCode() === "4242703000" && $e->errorInfo[1] === 7) { + return true; + } + + throw $e; + } } /** From d148ebab23ee68130c31dc9b2950ddaf03ea23c9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 18:07:55 +1300 Subject: [PATCH 114/256] Add transaction exception --- src/Database/Adapter/SQL.php | 13 +++++++------ src/Database/Exception/Transaction.php | 9 +++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 src/Database/Exception/Transaction.php diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 745d2e59a..6023212af 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -9,6 +9,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; abstract class SQL extends Adapter @@ -43,11 +44,11 @@ public function startTransaction(): bool $result = true; } } catch (PDOException $e) { - throw new DatabaseException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); + throw new TransactionException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); } if (!$result) { - throw new DatabaseException('Failed to start transaction'); + throw new TransactionException('Failed to start transaction'); } $this->inTransaction++; @@ -70,13 +71,13 @@ public function commitTransaction(): bool try { $result = $this->getPDO()->commit(); } catch (PDOException $e) { - throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); + throw new TransactionException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { $this->inTransaction--; } if (!$result) { - throw new DatabaseException('Failed to commit transaction'); + throw new TransactionException('Failed to commit transaction'); } return $result; @@ -94,13 +95,13 @@ public function rollbackTransaction(): bool try { $result = $this->getPDO()->rollBack(); } catch (PDOException $e) { - throw new DatabaseException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); + throw new TransactionException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { $this->inTransaction = 0; } if (!$result) { - throw new DatabaseException('Failed to rollback transaction'); + throw new TransactionException('Failed to rollback transaction'); } return $result; diff --git a/src/Database/Exception/Transaction.php b/src/Database/Exception/Transaction.php new file mode 100644 index 000000000..3a3ddf0af --- /dev/null +++ b/src/Database/Exception/Transaction.php @@ -0,0 +1,9 @@ + Date: Tue, 5 Nov 2024 18:08:21 +1300 Subject: [PATCH 115/256] Handle possible implicit commit --- src/Database/Adapter.php | 33 ++++++++++++++++++++++++--------- src/Database/Adapter/SQL.php | 5 +++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index f0f63b659..3b7aedd91 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -315,16 +315,31 @@ public function inTransaction(): bool */ public function withTransaction(callable $callback): mixed { - $this->startTransaction(); - - try { - $result = $callback(); - $this->commitTransaction(); - return $result; - } catch (\Throwable $e) { - $this->rollbackTransaction(); - throw $e; + for ($attempts = 0; $attempts < 3; $attempts++) { + try { + $this->startTransaction(); + $result = $callback(); + $this->commitTransaction(); + return $result; + } catch (\Throwable $e) { + + try { + $this->rollbackTransaction(); + } catch (\Throwable) { + if ($attempts < 2) { + continue; + } + } + + if ($attempts < 2) { + continue; + } + + throw $e; + } } + + throw new TransactionException('Failed to execute transaction'); } /** diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 6023212af..06b7136d2 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -68,6 +68,11 @@ public function commitTransaction(): bool return true; } + if (!$this->getPDO()->inTransaction()) { + $this->inTransaction = 0; + return false; + } + try { $result = $this->getPDO()->commit(); } catch (PDOException $e) { From 6c10f97e50131e9fc655463064365a31480c0098 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 18:09:17 +1300 Subject: [PATCH 116/256] Reset counter to 0 only if commit succeeded --- src/Database/Adapter/SQL.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 06b7136d2..fbf3ad887 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -75,10 +75,9 @@ public function commitTransaction(): bool try { $result = $this->getPDO()->commit(); + $this->inTransaction = 0; } catch (PDOException $e) { throw new TransactionException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); - } finally { - $this->inTransaction--; } if (!$result) { From d64ce93f26e9b952b1ff2d98d51e90a1fbb229fc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 18:11:16 +1300 Subject: [PATCH 117/256] Savepoints for nested transactions --- src/Database/Adapter/SQL.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index fbf3ad887..64ce0235e 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -41,6 +41,7 @@ public function startTransaction(): bool $result = $this->getPDO()->beginTransaction(); } else { + $this->getPDO()->exec('SAVEPOINT transaction' . $this->inTransaction); $result = true; } } catch (PDOException $e) { @@ -97,11 +98,16 @@ public function rollbackTransaction(): bool } try { - $result = $this->getPDO()->rollBack(); + if ($this->inTransaction > 1) { + $this->getPDO()->exec('ROLLBACK TO transaction' . $this->inTransaction); + $this->inTransaction--; + $result = true; + } else { + $result = $this->getPDO()->rollBack(); + $this->inTransaction = 0; + } } catch (PDOException $e) { - throw new TransactionException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); - } finally { - $this->inTransaction = 0; + throw new DatabaseException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); } if (!$result) { From 3b2b22ecb8a7683c3a06b9849a27548e5c46ae53 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 18:56:13 +1300 Subject: [PATCH 118/256] Remove savepoints from postgres --- src/Database/Adapter.php | 1 - src/Database/Adapter/Postgres.php | 52 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 3b7aedd91..d78822fba 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -322,7 +322,6 @@ public function withTransaction(callable $callback): mixed $this->commitTransaction(); return $result; } catch (\Throwable $e) { - try { $this->rollbackTransaction(); } catch (\Throwable) { diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 983e112dd..4fa08fcb1 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -11,6 +11,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; @@ -26,6 +27,57 @@ class Postgres extends SQL * 4. Full-text search is different - to_tsvector() and to_tsquery() */ + /** + * @inheritDoc + */ + public function startTransaction(): bool + { + try { + if ($this->inTransaction === 0) { + if ($this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); + } + + $result = $this->getPDO()->beginTransaction(); + } else { + $result = true; + } + } catch (PDOException $e) { + throw new TransactionException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); + } + + if (!$result) { + throw new TransactionException('Failed to start transaction'); + } + + $this->inTransaction++; + + return $result; + } + + /** + * @inheritDoc + */ + public function rollbackTransaction(): bool + { + if ($this->inTransaction === 0) { + return false; + } + + try { + $result = $this->getPDO()->rollBack(); + $this->inTransaction = 0; + } catch (PDOException $e) { + throw new DatabaseException('Failed to rollback transaction: ' . $e->getMessage(), $e->getCode(), $e); + } + + if (!$result) { + throw new TransactionException('Failed to rollback transaction'); + } + + return $result; + } + /** * Returns Max Execution Time * @param int $milliseconds From 160110f2750aca3eb10592a5219d472d3f8261e0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 19:23:59 +1300 Subject: [PATCH 119/256] Issue raw rollback to be certain no transactions are active --- src/Database/Adapter/SQL.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 64ce0235e..f03035c4b 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -37,6 +37,9 @@ public function startTransaction(): bool if ($this->inTransaction === 0) { if ($this->getPDO()->inTransaction()) { $this->getPDO()->rollBack(); + } else { + // If no active transaction, this has no effect. + $this->getPDO()->exec('ROLLBACK'); } $result = $this->getPDO()->beginTransaction(); From 6f9b6023923a318e5f7c2a879cd3c7616d30df1f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 19:28:22 +1300 Subject: [PATCH 120/256] Throw rollback exception if max attempts reached --- src/Database/Adapter.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index d78822fba..e01c0e006 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -321,20 +321,23 @@ public function withTransaction(callable $callback): mixed $result = $callback(); $this->commitTransaction(); return $result; - } catch (\Throwable $e) { + } catch (\Throwable $action) { + try { $this->rollbackTransaction(); - } catch (\Throwable) { + } catch (\Throwable $rollback) { if ($attempts < 2) { continue; } + + throw $rollback; } if ($attempts < 2) { continue; } - throw $e; + throw $action; } } From 7b52365b3f7f70e81ac995098f034889aebf9cdd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 20:28:29 +1300 Subject: [PATCH 121/256] 5ms sleep --- src/Database/Adapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index e01c0e006..a60d105c8 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -327,6 +327,7 @@ public function withTransaction(callable $callback): mixed $this->rollbackTransaction(); } catch (\Throwable $rollback) { if ($attempts < 2) { + \usleep(5000); // 5ms continue; } @@ -334,6 +335,7 @@ public function withTransaction(callable $callback): mixed } if ($attempts < 2) { + \usleep(5000); // 5ms continue; } From 1158f910d590816334415a2332b265f2f6c3a675 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 21:20:24 +1300 Subject: [PATCH 122/256] Fix stan --- src/Database/Adapter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index a60d105c8..fd59b8300 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -6,6 +6,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Transaction as TransactionException; abstract class Adapter { From 8d1b7dc0d3492234f8ce7aa0b8e4270384d05a5e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 5 Nov 2024 22:16:58 +1300 Subject: [PATCH 123/256] Prepared statements for savepoints --- src/Database/Adapter/SQL.php | 13 ++++++++----- src/Database/Adapter/SQLite.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index f03035c4b..3b837dfd5 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -39,13 +39,14 @@ public function startTransaction(): bool $this->getPDO()->rollBack(); } else { // If no active transaction, this has no effect. - $this->getPDO()->exec('ROLLBACK'); + $this->getPDO()->prepare('ROLLBACK')->execute(); } $result = $this->getPDO()->beginTransaction(); } else { - $this->getPDO()->exec('SAVEPOINT transaction' . $this->inTransaction); - $result = true; + $result = $this->getPDO() + ->prepare('SAVEPOINT transaction' . $this->inTransaction) + ->execute(); } } catch (PDOException $e) { throw new TransactionException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); @@ -102,9 +103,11 @@ public function rollbackTransaction(): bool try { if ($this->inTransaction > 1) { - $this->getPDO()->exec('ROLLBACK TO transaction' . $this->inTransaction); + $result = $this->getPDO() + ->prepare('ROLLBACK TO transaction' . ($this->inTransaction - 1)) + ->execute(); + $this->inTransaction--; - $result = true; } else { $result = $this->getPDO()->rollBack(); $this->inTransaction = 0; diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5d1945598..0b6bc7b99 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -12,6 +12,7 @@ use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Helpers\ID; /** @@ -30,6 +31,36 @@ */ class SQLite extends MariaDB { + /** + * @inheritDoc + */ + public function startTransaction(): bool + { + try { + if ($this->inTransaction === 0) { + if ($this->getPDO()->inTransaction()) { + $this->getPDO()->rollBack(); + } + + $result = $this->getPDO()->beginTransaction(); + } else { + $result = $this->getPDO() + ->prepare('SAVEPOINT transaction' . $this->inTransaction) + ->execute(); + } + } catch (PDOException $e) { + throw new TransactionException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); + } + + if (!$result) { + throw new TransactionException('Failed to start transaction'); + } + + $this->inTransaction++; + + return $result; + } + /** * Check if Database exists * Optionally check if collection exists in Database From a32165c193fff73d326eff8460d4c94bb992f81c Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 5 Nov 2024 17:45:58 +0200 Subject: [PATCH 124/256] Get connection id --- src/Database/Adapter.php | 14 ++++++++++++++ src/Database/Adapter/MariaDB.php | 9 +++++++++ src/Database/Adapter/Mongo.php | 15 +++++++++++++++ src/Database/Adapter/Postgres.php | 9 +++++++++ src/Database/Adapter/SQL.php | 10 ++++++++++ src/Database/Adapter/SQLite.php | 10 ++++++++++ src/Database/Database.php | 11 +++++++++++ tests/e2e/Adapter/Base.php | 12 ++++++++++++ 8 files changed, 90 insertions(+) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index f0f63b659..4bb576d8c 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -833,6 +833,13 @@ abstract public function getSupportForBatchOperations(): bool; */ abstract public function getSupportForAttributeResizing(): bool; + /** + * Are attributes supported? + * + * @return bool + */ + abstract public function getSupportForGetConnectionId(): bool; + /** * Get current attribute count from collection document * @@ -980,4 +987,11 @@ public function escapeWildcards(string $value): string * @throws Exception */ abstract public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool; + + /** + * Returns the connection ID identifier + * + * @return string + */ + abstract public function getConnectionId(): string; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0e72eee4e..964a0626a 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2398,4 +2398,13 @@ protected function processException(PDOException $e): void throw $e; } + + /** + * @return string + */ + public function getConnectionId(): string + { + $stmt = $this->getPDO()->query("SELECT CONNECTION_ID();"); + return $stmt->fetchColumn(); + } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 433068396..14d58aff1 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1777,6 +1777,16 @@ public function getSupportForBatchOperations(): bool return false; } + /** + * Is get connection id supported? + * + * @return bool + */ + public function getSupportForGetConnectionId(): bool + { + return false; + } + /** * Get current attribute count from collection document * @@ -1926,4 +1936,9 @@ public function getMaxIndexLength(): int { return 0; } + + public function getConnectionId(): string + { + return '0'; + } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 983e112dd..ecc6ae905 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2337,4 +2337,13 @@ protected function processException(PDOException $e): void throw $e; } + + /** + * @return string + */ + public function getConnectionId(): string + { + $stmt = $this->getPDO()->query("SELECT pg_backend_pid();"); + return $stmt->fetchColumn(); + } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 745d2e59a..2a384d543 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -375,6 +375,16 @@ public function getSupportForBatchOperations(): bool return true; } + /** + * Is get connection id supported? + * + * @return bool + */ + public function getSupportForGetConnectionId(): bool + { + return true; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5d1945598..44941b36a 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -866,6 +866,16 @@ public function getSupportForAttributeResizing(): bool return false; } + /** + * Is get connection id supported? + * + * @return bool + */ + public function getSupportForGetConnectionId(): bool + { + return false; + } + /** * Get SQL Index Type * diff --git a/src/Database/Database.php b/src/Database/Database.php index 13645e36f..2fe136ef8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -493,6 +493,17 @@ public function silent(callable $callback, array $listeners = null): mixed } } + /** + * Get getConnection Id + * + * @return string + * @throws Exception + */ + public function getConnectionId(): string + { + return $this->adapter->getConnectionId(); + } + /** * Skip relationships for all the calls inside the callback * diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6fde24f1f..ab3a3784f 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -99,6 +99,18 @@ public function testUpdateDeleteCollectionNotFound(): void } } + public function testGetCollectionId(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForGetConnectionId()) { + $this->expectNotToPerformAssertions(); + return; + } + + $this->assertIsString(static::getDatabase()->getConnectionId()); + + $this->assertEquals(999, 11111); + } + public function testDeleteRelatedCollection(): void { if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { From ca07a57dc8c051a819ba7e426ceb24e11f69a8ab Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 5 Nov 2024 17:52:08 +0200 Subject: [PATCH 125/256] Remove comment --- tests/e2e/Adapter/Base.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index ab3a3784f..dcc0c5e2c 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -107,8 +107,6 @@ public function testGetCollectionId(): void } $this->assertIsString(static::getDatabase()->getConnectionId()); - - $this->assertEquals(999, 11111); } public function testDeleteRelatedCollection(): void From 872012b5cce54d3dc1c04238787f9d6cdbfdc630 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Tue, 5 Nov 2024 18:06:22 +0100 Subject: [PATCH 126/256] chore: update utopia-php/cache to 0.11.0 --- composer.json | 2 +- composer.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index a1ca02e4b..66b0fad6d 100755 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "ext-mbstring": "*", "php": ">=8.3", "utopia-php/framework": "0.33.*", - "utopia-php/cache": "0.10.*", + "utopia-php/cache": "0.11.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 49c2e3ddf..1a149c62a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8b51c1a58da391a80c01cd5b691b739", + "content-hash": "f7eec4bad737b741ae97c81db0532d29", "packages": [ { "name": "jean85/pretty-package-versions", @@ -216,16 +216,16 @@ }, { "name": "utopia-php/cache", - "version": "0.10.2", + "version": "0.11.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" + "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", - "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/8ebcab5aac7606331cef69b0081f6c9eff2e58bc", + "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc", "shasum": "" }, "require": { @@ -236,7 +236,7 @@ }, "require-dev": { "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", + "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.13.1" }, @@ -260,9 +260,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.10.2" + "source": "https://github.com/utopia-php/cache/tree/0.11.0" }, - "time": "2024-06-25T20:36:35+00:00" + "time": "2024-11-05T16:53:58+00:00" }, { "name": "utopia-php/framework", @@ -2591,7 +2591,7 @@ "platform": { "ext-pdo": "*", "ext-mbstring": "*", - "php": ">=8.0" + "php": ">=8.3" }, "platform-dev": [], "plugin-api-version": "2.6.0" From a21113139cc22c471ea11b911bde8a2cdc28c49c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 14:35:32 +1300 Subject: [PATCH 127/256] Reset transaction count on retry loop failure --- docker-compose.yml | 2 +- src/Database/Adapter.php | 3 ++- src/Database/Adapter/Postgres.php | 3 +++ src/Database/Adapter/SQL.php | 9 ++------- tests/e2e/Adapter/Base.php | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 23b135ba9..5497f4e28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: . args: - DEBUG: false + DEBUG: true networks: - database volumes: diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index fd59b8300..2b0cb04fd 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -323,7 +323,6 @@ public function withTransaction(callable $callback): mixed $this->commitTransaction(); return $result; } catch (\Throwable $action) { - try { $this->rollbackTransaction(); } catch (\Throwable $rollback) { @@ -332,6 +331,7 @@ public function withTransaction(callable $callback): mixed continue; } + $this->inTransaction = 0; throw $rollback; } @@ -340,6 +340,7 @@ public function withTransaction(callable $callback): mixed continue; } + $this->inTransaction = 0; throw $action; } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 4fa08fcb1..4bd39827c 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -36,6 +36,9 @@ public function startTransaction(): bool if ($this->inTransaction === 0) { if ($this->getPDO()->inTransaction()) { $this->getPDO()->rollBack(); + } else { + // If no active transaction, this has no effect. + $this->getPDO()->prepare('ROLLBACK')->execute(); } $result = $this->getPDO()->beginTransaction(); diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 3b837dfd5..abf74e987 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -44,9 +44,7 @@ public function startTransaction(): bool $result = $this->getPDO()->beginTransaction(); } else { - $result = $this->getPDO() - ->prepare('SAVEPOINT transaction' . $this->inTransaction) - ->execute(); + $result = $this->getPDO()->exec('SAVEPOINT transaction' . $this->inTransaction); } } catch (PDOException $e) { throw new TransactionException('Failed to start transaction: ' . $e->getMessage(), $e->getCode(), $e); @@ -103,10 +101,7 @@ public function rollbackTransaction(): bool try { if ($this->inTransaction > 1) { - $result = $this->getPDO() - ->prepare('ROLLBACK TO transaction' . ($this->inTransaction - 1)) - ->execute(); - + $result = $this->getPDO()->exec('ROLLBACK TO transaction' . ($this->inTransaction - 1)); $this->inTransaction--; } else { $result = $this->getPDO()->rollBack(); diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6fde24f1f..b4c3a8c5f 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -348,7 +348,7 @@ public function testVirtualRelationsAttributes(): void ])); $this->fail('Failed to throw exception'); } catch (Exception $e) { - $this->assertTrue($e instanceof RelationshipException); + $this->assertInstanceOf(RelationshipException::class, $e); } static::getDatabase()->deleteRelationship('v1', 'v2'); From 4ed35fdc2386721e00215574ac4964ba978e27fc Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 14:36:53 +1300 Subject: [PATCH 128/256] Dummy --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index ccdaa969e..783265d80 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/unit From d9f85c67b10cf2064955644237b2c7af37629bcb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 14:36:58 +1300 Subject: [PATCH 129/256] Revert "Dummy" This reverts commit 4ed35fdc2386721e00215574ac4964ba978e27fc. --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 783265d80..ccdaa969e 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true"> + stopOnFailure="false"> ./tests/unit From 3b4b2492e5d0888fc254a31b2644764ca212da65 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 15:10:19 +1300 Subject: [PATCH 130/256] Ensure values is array --- src/Database/Query.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Database/Query.php b/src/Database/Query.php index aab17c195..1c82e7b4c 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -262,6 +262,10 @@ public static function parseQuery(array $query): self throw new QueryException('Invalid query attribute. Must be a string, got ' . \gettype($attribute)); } + if (!\is_array($values)) { + throw new QueryException('Invalid query values. Must be an array, got ' . \gettype($values)); + } + if (\in_array($method, self::LOGICAL_TYPES)) { foreach ($values as $index => $value) { $values[$index] = self::parseQuery($value); From c575e50d89976c7286d08dcb138ac50e26bd70ed Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 15:15:49 +1300 Subject: [PATCH 131/256] Ensure method is string --- src/Database/Query.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Database/Query.php b/src/Database/Query.php index 1c82e7b4c..7388453b2 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -254,6 +254,10 @@ public static function parseQuery(array $query): self $attribute = $query['attribute'] ?? ''; $values = $query['values'] ?? []; + if (!\is_string($method)) { + throw new QueryException('Invalid query method. Must be a string, got ' . \gettype($method)); + } + if (!self::isMethod($method)) { throw new QueryException('Invalid query method: ' . $method); } From 9fe321165712bdbdefecd192b51fb7c889acc4a9 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 15:15:54 +1300 Subject: [PATCH 132/256] Add tests --- tests/unit/QueryTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 9666ebf3a..d9ad6cd93 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -213,6 +213,13 @@ public function testParse(): void $this->assertEquals('actors', $queries[0]->getAttribute()); $this->assertEquals($json, '{"method":"or","values":[{"method":"equal","attribute":"actors","values":["Brad Pitt"]},{"method":"equal","attribute":"actors","values":["Johnny Depp"]}]}'); + try { + Query::parse('{"method":["equal"],"attribute":["title"],"values":["test"]}'); + $this->fail('Failed to throw exception'); + } catch (QueryException $e) { + $this->assertEquals('Invalid query method. Must be a string, got array', $e->getMessage()); + } + try { Query::parse('{"method":"equal","attribute":["title"],"values":["test"]}'); $this->fail('Failed to throw exception'); @@ -220,6 +227,13 @@ public function testParse(): void $this->assertEquals('Invalid query attribute. Must be a string, got array', $e->getMessage()); } + try { + Query::parse('{"method":"equal","attribute":"title","values":"test"}'); + $this->fail('Failed to throw exception'); + } catch (QueryException $e) { + $this->assertEquals('Invalid query values. Must be an array, got string', $e->getMessage()); + } + try { Query::parse('false'); $this->fail('Failed to throw exception'); From 7383ca946a197118058929b9dc60f9a973fdec13 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 15:19:22 +1300 Subject: [PATCH 133/256] Revert "Add tests" This reverts commit 9fe321165712bdbdefecd192b51fb7c889acc4a9. --- tests/unit/QueryTest.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index d9ad6cd93..9666ebf3a 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -213,13 +213,6 @@ public function testParse(): void $this->assertEquals('actors', $queries[0]->getAttribute()); $this->assertEquals($json, '{"method":"or","values":[{"method":"equal","attribute":"actors","values":["Brad Pitt"]},{"method":"equal","attribute":"actors","values":["Johnny Depp"]}]}'); - try { - Query::parse('{"method":["equal"],"attribute":["title"],"values":["test"]}'); - $this->fail('Failed to throw exception'); - } catch (QueryException $e) { - $this->assertEquals('Invalid query method. Must be a string, got array', $e->getMessage()); - } - try { Query::parse('{"method":"equal","attribute":["title"],"values":["test"]}'); $this->fail('Failed to throw exception'); @@ -227,13 +220,6 @@ public function testParse(): void $this->assertEquals('Invalid query attribute. Must be a string, got array', $e->getMessage()); } - try { - Query::parse('{"method":"equal","attribute":"title","values":"test"}'); - $this->fail('Failed to throw exception'); - } catch (QueryException $e) { - $this->assertEquals('Invalid query values. Must be an array, got string', $e->getMessage()); - } - try { Query::parse('false'); $this->fail('Failed to throw exception'); From b7110d5fe5d90c2d264a59e665a4730b9aad9675 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 15:19:28 +1300 Subject: [PATCH 134/256] Reapply "Add tests" This reverts commit 7383ca946a197118058929b9dc60f9a973fdec13. --- tests/unit/QueryTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 9666ebf3a..d9ad6cd93 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -213,6 +213,13 @@ public function testParse(): void $this->assertEquals('actors', $queries[0]->getAttribute()); $this->assertEquals($json, '{"method":"or","values":[{"method":"equal","attribute":"actors","values":["Brad Pitt"]},{"method":"equal","attribute":"actors","values":["Johnny Depp"]}]}'); + try { + Query::parse('{"method":["equal"],"attribute":["title"],"values":["test"]}'); + $this->fail('Failed to throw exception'); + } catch (QueryException $e) { + $this->assertEquals('Invalid query method. Must be a string, got array', $e->getMessage()); + } + try { Query::parse('{"method":"equal","attribute":["title"],"values":["test"]}'); $this->fail('Failed to throw exception'); @@ -220,6 +227,13 @@ public function testParse(): void $this->assertEquals('Invalid query attribute. Must be a string, got array', $e->getMessage()); } + try { + Query::parse('{"method":"equal","attribute":"title","values":"test"}'); + $this->fail('Failed to throw exception'); + } catch (QueryException $e) { + $this->assertEquals('Invalid query values. Must be an array, got string', $e->getMessage()); + } + try { Query::parse('false'); $this->fail('Failed to throw exception'); From 33576578f9b4014b2a5acb2c2574f85145299f0f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 16:39:25 +1300 Subject: [PATCH 135/256] Revert "Question?" This reverts commit df574633d6540984e5ae8efab806e3b493acca5b. --- src/Database/Adapter/SQL.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index cc49313b4..cc899ceaf 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -91,7 +91,6 @@ public function commitTransaction(): bool throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), $e->getCode(), $e); } finally { \var_dump('Decrementing transaction count'); - // Should we set this to 0 if we commit? $this->inTransaction--; } From cf9ba5a268bfe5deb01c8e716409b10fb47069b9 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 6 Nov 2024 13:40:04 +0900 Subject: [PATCH 136/256] Make bulk document deletes support all queries --- src/Database/Database.php | 69 +++++++++++++++++++++++++++------ tests/e2e/Adapter/Base.php | 78 +++++++++++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 13645e36f..25ddadba5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3939,9 +3939,22 @@ public function updateDocuments(string $collection, Document $updates, array $qu return 0; } - $queries = Query::groupByType($queries)['filters']; $collection = $this->silent(fn () => $this->getCollection($collection)); + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + if ($this->validate) { + $validator = new DocumentsValidator($attributes, $indexes); + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + } + unset($updates['$id']); unset($updates['$createdAt']); unset($updates['$tenant']); @@ -5090,13 +5103,34 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } - $queries = Query::groupByType($queries)['filters']; $collection = $this->silent(fn () => $this->getCollection($collection)); - $affectedDocumentIds = []; - $deleted = $this->withTransaction(function () use ($collection, $queries, $batchSize, $affectedDocumentIds) { - $lastDocument = null; + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + if ($this->validate) { + $validator = new DocumentsValidator($attributes, $indexes); + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + } + $grouped = Query::groupByType($queries); + $filters = $grouped['filters']; + $limit = $grouped['limit']; + $cursor = $grouped['cursor']; + + if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } + + $affectedDocumentIds = []; + + $deleted = $this->withTransaction(function () use ($collection, $queries, $batchSize, $affectedDocumentIds, $limit, $cursor) { $documentSecurity = $collection->getAttribute('documentSecurity', false); $authorization = new Authorization(self::PERMISSION_DELETE); $skipAuth = $authorization->isValid($collection->getDelete()); @@ -5105,16 +5139,27 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba throw new AuthorizationException($authorization->getDescription()); } + $originalLimit = $limit; + $lastDocument = $cursor; + while (true) { - $affectedDocuments = $this->find($collection->getId(), array_merge( + if ($limit && $limit < $batchSize) { + $batchSize = $limit; + } elseif (!empty($limit)) { + $limit -= $batchSize; + } + + $queries = array_merge( + $queries, empty($lastDocument) ? [ Query::limit($batchSize), ] : [ Query::limit($batchSize), Query::cursorAfter($lastDocument), - ], - $queries, - ), forPermission: Database::PERMISSION_DELETE); + ] + ); + + $affectedDocuments = $this->find($collection->getId(), $queries, forPermission: Database::PERMISSION_DELETE); if (empty($affectedDocuments)) { break; @@ -5134,9 +5179,11 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba if (count($affectedDocuments) < $batchSize) { break; - } else { - $lastDocument = end($affectedDocuments); + } elseif ($originalLimit && count($affectedDocumentIds) == $originalLimit) { + break; } + + $lastDocument = end($affectedDocuments); } if (empty($affectedDocumentIds)) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6fde24f1f..aa02df2d2 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15647,9 +15647,9 @@ public function testTransformations(): void $this->assertTrue($result->isEmpty()); } - public function propegateBulkDocuments(bool $documentSecurity = false): void + public function propegateBulkDocuments(bool $documentSecurity = false, int $amount = 10): void { - for ($i = 0; $i < 10; $i++) { + for ($i = 0; $i < $amount; $i++) { static::getDatabase()->createDocument('bulk_delete', new Document( array_merge([ '$id' => 'doc' . $i, @@ -15667,6 +15667,11 @@ public function propegateBulkDocuments(bool $documentSecurity = false): void public function testDeleteBulkDocuments(): void { + if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + static::getDatabase()->createCollection( 'bulk_delete', attributes: [ @@ -15759,9 +15764,70 @@ public function testDeleteBulkDocuments(): void static::getDatabase()->deleteCollection('bulk_delete'); } + public function testDeleteBulkDocumentsQueries(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + static::getDatabase()->createCollection( + 'bulk_delete', + attributes: [ + new Document([ + '$id' => 'text', + 'type' => Database::VAR_STRING, + 'size' => 100, + 'required' => true, + ]), + new Document([ + '$id' => 'integer', + 'type' => Database::VAR_INTEGER, + 'size' => 10, + 'required' => true, + ]) + ], + documentSecurity: false, + permissions: [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::delete(Role::any()) + ] + ); + + // Test limit + $this->propegateBulkDocuments(); + + $this->assertEquals(5, static::getDatabase()->deleteDocuments('bulk_delete', [Query::limit(5)])); + $this->assertCount(5, static::getDatabase()->find('bulk_delete')); + + $this->assertEquals(5, static::getDatabase()->deleteDocuments('bulk_delete', [Query::limit(5)])); + $this->assertCount(0, static::getDatabase()->find('bulk_delete')); + + // Test Limit more than batchSize + $this->propegateBulkDocuments(false, Database::DELETE_BATCH_SIZE * 2); + $this->assertCount(Database::DELETE_BATCH_SIZE * 2, static::getDatabase()->find('bulk_delete', [Query::limit(200)])); + + $this->assertEquals(Database::DELETE_BATCH_SIZE + 2, static::getDatabase()->deleteDocuments('bulk_delete', [Query::limit(Database::DELETE_BATCH_SIZE + 2)])); + + $this->assertCount(Database::DELETE_BATCH_SIZE - 2, static::getDatabase()->find('bulk_delete', [Query::limit(200)])); + $this->assertEquals(Database::DELETE_BATCH_SIZE - 2, $this->getDatabase()->deleteDocuments('bulk_delete')); + + // Test Offset + $this->propegateBulkDocuments(false, 100); + $this->assertEquals(50, static::getDatabase()->deleteDocuments('bulk_delete', [Query::offset(50)])); + + $docs = static::getDatabase()->find('bulk_delete', [Query::limit(100)]); + $this->assertCount(50, $docs); + $this->assertEquals('doc49', end($docs)->getId()); + $this->assertEquals(50, static::getDatabase()->deleteDocuments('bulk_delete')); + + static::getDatabase()->deleteCollection('bulk_delete'); + } + public function testDeleteBulkDocumentsOneToOneRelationship(): void { - if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships() || !static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { $this->expectNotToPerformAssertions(); return; } @@ -15957,7 +16023,7 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void public function testDeleteBulkDocumentsOneToManyRelationship(): void { - if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships() || !static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { $this->expectNotToPerformAssertions(); return; } @@ -16136,7 +16202,7 @@ public function testDeleteBulkDocumentsOneToManyRelationship(): void public function testDeleteBulkDocumentsManyToManyRelationship(): void { - if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships() || !static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { $this->expectNotToPerformAssertions(); return; } @@ -16211,7 +16277,7 @@ public function testDeleteBulkDocumentsManyToManyRelationship(): void public function testDeleteBulkDocumentsManyToOneRelationship(): void { - if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships() || !static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { $this->expectNotToPerformAssertions(); return; } From 55bfbfc88f4ff714a7f970dbc40c43c82cc4113d Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 6 Nov 2024 16:05:16 +0900 Subject: [PATCH 137/256] Implement support for all queries on bulk document updates --- composer.lock | 4 +-- src/Database/Database.php | 52 +++++++++++++++++++++++----- tests/e2e/Adapter/Base.php | 71 ++++++++++++++++++++++++++++++++++---- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/composer.lock b/composer.lock index 1a149c62a..084be7692 100644 --- a/composer.lock +++ b/composer.lock @@ -2585,7 +2585,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -2593,6 +2593,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/Database/Database.php b/src/Database/Database.php index 13645e36f..8511ce4cf 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3939,9 +3939,31 @@ public function updateDocuments(string $collection, Document $updates, array $qu return 0; } - $queries = Query::groupByType($queries)['filters']; $collection = $this->silent(fn () => $this->getCollection($collection)); + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + if ($this->validate) { + $validator = new DocumentsValidator($attributes, $indexes); + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + } + + $grouped = Query::groupByType($queries); + $filters = $grouped['filters']; + $limit = $grouped['limit']; + $cursor = $grouped['cursor']; + + if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } + unset($updates['$id']); unset($updates['$createdAt']); unset($updates['$tenant']); @@ -3958,11 +3980,12 @@ public function updateDocuments(string $collection, Document $updates, array $qu $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime(), ); + if (!$validator->isValid($updates)) { throw new StructureException($validator->getDescription()); } - $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates) { + $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor) { $lastDocument = null; $totalModified = 0; $affectedDocumentIds = []; @@ -3976,17 +3999,28 @@ public function updateDocuments(string $collection, Document $updates, array $qu throw new AuthorizationException($authorization->getDescription()); } + $originalLimit = $limit; + $lastDocument = $cursor; + // Resolve and update relationships while (true) { - $affectedDocuments = $this->find($collection->getId(), array_merge( + if ($limit && $limit < $batchSize) { + $batchSize = $limit; + } elseif (!empty($limit)) { + $limit -= $batchSize; + } + + $queries = array_merge( + $queries, empty($lastDocument) ? [ Query::limit($batchSize), ] : [ Query::limit($batchSize), Query::cursorAfter($lastDocument), - ], - $queries, - ), forPermission: Database::PERMISSION_UPDATE); + ] + ); + + $affectedDocuments = $this->find($collection->getId(), $queries, forPermission: Database::PERMISSION_UPDATE); if (empty($affectedDocuments)) { break; @@ -4014,9 +4048,11 @@ public function updateDocuments(string $collection, Document $updates, array $qu if (count($affectedDocuments) < $batchSize) { break; - } else { - $lastDocument = end($affectedDocuments); + } elseif ($originalLimit && count($affectedDocumentIds) == $originalLimit) { + break; } + + $lastDocument = end($affectedDocuments); } $this->trigger(self::EVENT_DOCUMENTS_UPDATE, $affectedDocumentIds); diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b4c3a8c5f..1f3f4cea6 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15647,10 +15647,10 @@ public function testTransformations(): void $this->assertTrue($result->isEmpty()); } - public function propegateBulkDocuments(bool $documentSecurity = false): void + public function propegateBulkDocuments(string $collection, int $amount = 10, bool $documentSecurity = false): void { - for ($i = 0; $i < 10; $i++) { - static::getDatabase()->createDocument('bulk_delete', new Document( + for ($i = 0; $i < $amount; $i++) { + static::getDatabase()->createDocument($collection, new Document( array_merge([ '$id' => 'doc' . $i, 'text' => 'value' . $i, @@ -15691,7 +15691,7 @@ public function testDeleteBulkDocuments(): void ] ); - $this->propegateBulkDocuments(); + $this->propegateBulkDocuments('bulk_delete'); $docs = static::getDatabase()->find('bulk_delete'); $this->assertCount(10, $docs); @@ -15704,7 +15704,7 @@ public function testDeleteBulkDocuments(): void $this->assertCount(0, $docs); // TEST: Bulk delete documents with queries. - $this->propegateBulkDocuments(); + $this->propegateBulkDocuments('bulk_delete'); $deleted = static::getDatabase()->deleteDocuments('bulk_delete', [ Query::greaterThanEqual('integer', 5) @@ -15736,7 +15736,7 @@ public function testDeleteBulkDocuments(): void static::getDatabase()->updateCollection('bulk_delete', [ Permission::create(Role::any()), ], true); - $this->propegateBulkDocuments(true); + $this->propegateBulkDocuments('bulk_delete', documentSecurity: true); $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); $this->assertEquals(0, $deleted); @@ -16463,6 +16463,65 @@ public function testUpdateDocuments(): void Authorization::setRole(Role::any()->toString()); } + public function testUpdateDocumentsQueries(): void + { + if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collection = 'testUpdateDocumentsQueries'; + + static::getDatabase()->createCollection($collection, attributes: [ + new Document([ + '$id' => ID::custom('text'), + 'type' => Database::VAR_STRING, + 'size' => 64, + 'required' => true, + ]), + new Document([ + '$id' => ID::custom('integer'), + 'type' => Database::VAR_INTEGER, + 'size' => 64, + 'required' => true, + ]), + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ], documentSecurity: true); + + // Test limit + $this->propegateBulkDocuments($collection, 100); + + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + 'text' => 'text📝 updated', + ]), [Query::limit(10)]); + + $this->assertEquals(10, $affected); + $this->assertCount(10, static::getDatabase()->find($collection, [Query::equal('text', ['text📝 updated'])])); + $this->assertEquals(100, static::getDatabase()->deleteDocuments($collection)); + $this->assertCount(0, static::getDatabase()->find($collection)); + + // Test Offset + $this->propegateBulkDocuments($collection, 100); + + $affected = static::getDatabase()->updateDocuments($collection, new Document([ + 'text' => 'text📝 updated', + ]), [Query::offset(50)]); + + $this->assertEquals(50, $affected); + $docs = static::getDatabase()->find($collection, [Query::equal('text', ['text📝 updated']), Query::limit(100)]); + $this->assertCount(50, $docs); + + $lastDoc = end($docs); + $this->assertNotEmpty($lastDoc); + $this->assertEquals('doc99', $lastDoc->getId()); + + $this->assertEquals(100, static::getDatabase()->deleteDocuments($collection)); + } + public function testUpdateDocumentsPermissions(): void { if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations()) { From 48a7ede4b4d0a64a2cbc9f355cb2938842efccda Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 6 Nov 2024 16:08:19 +0900 Subject: [PATCH 138/256] Address Comments --- src/Database/Database.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 25ddadba5..c3cb3c52a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5113,14 +5113,20 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime() + ); + if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } } $grouped = Query::groupByType($queries); - $filters = $grouped['filters']; $limit = $grouped['limit']; $cursor = $grouped['cursor']; From 2ec40d77bf797e8bc8a137f7a26eb05c1a9baf2d Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 6 Nov 2024 16:13:14 +0900 Subject: [PATCH 139/256] Run Linter and fix PHPStan --- src/Database/Database.php | 8 ++++---- tests/e2e/Adapter/Base.php | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index c3cb3c52a..d2c311e11 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5114,10 +5114,10 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba if ($this->validate) { $validator = new DocumentsValidator( - $attributes, - $indexes, - $this->maxQueryValues, - $this->adapter->getMinDateTime(), + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), $this->adapter->getMaxDateTime() ); diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6cc378c5b..590a57457 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15819,7 +15819,10 @@ public function testDeleteBulkDocumentsQueries(): void $docs = static::getDatabase()->find('bulk_delete', [Query::limit(100)]); $this->assertCount(50, $docs); - $this->assertEquals('doc49', end($docs)->getId()); + + $lastDoc = end($docs); + $this->assertNotEmpty($lastDoc); + $this->assertEquals('doc49', $lastDoc->getId()); $this->assertEquals(50, static::getDatabase()->deleteDocuments('bulk_delete')); static::getDatabase()->deleteCollection('bulk_delete'); From c40d3201664c8ebd5b6f8d348d1bd9719725e142 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 6 Nov 2024 16:15:02 +0900 Subject: [PATCH 140/256] Add improved validation --- src/Database/Database.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 8511ce4cf..ceb43b93b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3949,7 +3949,14 @@ public function updateDocuments(string $collection, Document $updates, array $qu $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); + if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } From a23aec26bb3ab4f1e6a0aff137371f5061efeff6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 Nov 2024 20:49:40 +1300 Subject: [PATCH 141/256] Update index order --- src/Database/Adapter/MariaDB.php | 8 ++++---- src/Database/Adapter/Postgres.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e45189713..425360985 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -163,10 +163,10 @@ public function createCollection(string $name, array $attributes = [], array $in if ($this->sharedTables) { $collection .= " _tenant INT(11) UNSIGNED DEFAULT NULL, - UNIQUE KEY _uid (_tenant, _uid), - KEY _created_at (_tenant, _createdAt), - KEY _updated_at (_tenant, _updatedAt), - KEY _tenant_id (_tenant, _id) + UNIQUE KEY _uid (_uid, _tenant), + KEY _created_at (_createdAt, _tenant), + KEY _updated_at (_updatedAt, _tenant), + KEY _tenant_id (_id, _tenant) "; } else { $collection .= " diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 2bb044431..817ca3e2a 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -215,9 +215,9 @@ public function createCollection(string $name, array $attributes = [], array $in if ($this->sharedTables) { $collection .= " 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); + CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_created\" ON {$this->getSQLTable($id)} (\"_createdAt\", _tenant); + CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\", _tenant); + CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_tenant_id\" ON {$this->getSQLTable($id)} (_id, _tenant); "; } else { $collection .= " From 204177056faec1453c7f239b14a0ed9d6d66bbc2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 6 Nov 2024 10:12:05 +0200 Subject: [PATCH 142/256] Fix comment --- src/Database/Adapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 4bb576d8c..f58f8928f 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -834,7 +834,7 @@ abstract public function getSupportForBatchOperations(): bool; abstract public function getSupportForAttributeResizing(): bool; /** - * Are attributes supported? + * Is get connection id supported? * * @return bool */ From 90b80165f74f3af28198154986e2e913c2e78f23 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 6 Nov 2024 17:33:25 +0900 Subject: [PATCH 143/256] Fix infinite loop with bulk queries --- src/Database/Database.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index f713b3da5..d29580226 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4016,7 +4016,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu $limit -= $batchSize; } - $queries = array_merge( + $affectedDocuments = $this->find($collection->getId(), array_merge( $queries, empty($lastDocument) ? [ Query::limit($batchSize), @@ -4024,9 +4024,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu Query::limit($batchSize), Query::cursorAfter($lastDocument), ] - ); - - $affectedDocuments = $this->find($collection->getId(), $queries, forPermission: Database::PERMISSION_UPDATE); + ), forPermission: Database::PERMISSION_UPDATE); if (empty($affectedDocuments)) { break; @@ -5184,7 +5182,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $limit -= $batchSize; } - $queries = array_merge( + $affectedDocuments = $this->find($collection->getId(), array_merge( $queries, empty($lastDocument) ? [ Query::limit($batchSize), @@ -5192,9 +5190,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba Query::limit($batchSize), Query::cursorAfter($lastDocument), ] - ); - - $affectedDocuments = $this->find($collection->getId(), $queries, forPermission: Database::PERMISSION_DELETE); + ), forPermission: Database::PERMISSION_DELETE); if (empty($affectedDocuments)) { break; From e014f9f6ed0dc56498a477abf42433ec47cf611f Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 7 Nov 2024 15:46:22 +0900 Subject: [PATCH 144/256] Add tests, fix formatting and postgres error code --- src/Database/Adapter/MariaDB.php | 6 +++--- src/Database/Adapter/Postgres.php | 6 +++--- src/Database/Adapter/SQLite.php | 14 +++++++++++--- tests/e2e/Adapter/Base.php | 13 +++++++++++++ tests/e2e/Adapter/MariaDBTest.php | 12 ++++++++++++ tests/e2e/Adapter/MongoDBTest.php | 5 +++++ tests/e2e/Adapter/MySQLTest.php | 12 ++++++++++++ tests/e2e/Adapter/PostgresTest.php | 12 ++++++++++++ tests/e2e/Adapter/SQLiteTest.php | 12 ++++++++++++ tests/e2e/Adapter/SharedTables/MariaDBTest.php | 12 ++++++++++++ tests/e2e/Adapter/SharedTables/MongoDBTest.php | 5 +++++ tests/e2e/Adapter/SharedTables/MySQLTest.php | 12 ++++++++++++ tests/e2e/Adapter/SharedTables/PostgresTest.php | 12 ++++++++++++ tests/e2e/Adapter/SharedTables/SQLiteTest.php | 12 ++++++++++++ 14 files changed, 136 insertions(+), 9 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 40984e012..d110b54dc 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -421,8 +421,8 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa try { return $this->getPDO() - ->prepare($sql) - ->execute(); + ->prepare($sql) + ->execute(); } catch (PDOException $e) { if ($e->getCode() === "42000" && $e->errorInfo[1] === 1091) { return true; @@ -435,7 +435,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa /** * Rename Attribute * - * @param string $collection + * @param string $collectionp * @param string $old * @param string $new * @return bool diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 14986dfba..c60710d24 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -348,10 +348,10 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa try { return $this->getPDO() - ->prepare($sql) - ->execute(); + ->prepare($sql) + ->execute(); } catch (PDOException $e) { - if ($e->getCode() === "4242703000" && $e->errorInfo[1] === 7) { + if ($e->getCode() === "42703" && $e->errorInfo[1] === 7) { return true; } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5d1945598..70b28077f 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -341,9 +341,17 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa $sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql); - return $this->getPDO() - ->prepare($sql) - ->execute(); + try { + return $this->getPDO() + ->prepare($sql) + ->execute(); + } catch (PDOException $e) { + if (str_contains($e->getMessage(), 'no such column')) { + return true; + } + + throw $e; + } } /** diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6fde24f1f..105827870 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -41,6 +41,14 @@ abstract class Base extends TestCase */ abstract protected static function getDatabase(): Database; + /** + * @param string $collection + * @param string $column + * + * @return bool + */ + abstract protected static function deleteColumn(string $collection, string $column): bool; + /** * @return string */ @@ -1295,6 +1303,11 @@ public function testCreateDeleteAttribute(): void } catch (Exception $e) { $this->assertInstanceOf(DuplicateException::class, $e); } + + // Test delete attribute when column does not exist + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::deleteColumn('attributes', 'string1')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string1')); } /** diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index d7966e657..ca5b1988a 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -12,6 +12,7 @@ class MariaDBTest extends Base { protected static ?Database $database = null; + protected static ?PDO $pdo = null; protected static string $namespace; // Remove once all methods are implemented @@ -56,6 +57,17 @@ public static function getDatabase(bool $fresh = false): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index 3f3b5d5de..9bd560a8f 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -95,4 +95,9 @@ public function testKeywords(): void { $this->assertTrue(true); } + + protected static function deleteColumn(string $collection, string $column): bool + { + return true; + } } diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index 8d855d6ca..467ec820d 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -12,6 +12,7 @@ class MySQLTest extends Base { public static ?Database $database = null; + protected static ?PDO $pdo = null; protected static string $namespace; // Remove once all methods are implemented @@ -58,6 +59,17 @@ public static function getDatabase(): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index e024e4442..6834f9297 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -12,6 +12,7 @@ class PostgresTest extends Base { public static ?Database $database = null; + protected static ?PDO $pdo = null; protected static string $namespace; /** @@ -55,6 +56,17 @@ public static function getDatabase(): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = '"' . self::getDatabase()->getDatabase() . '"."' . self::getDatabase()->getNamespace() . '_' . $collection . '"'; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN \"{$column}\""; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index 2d4240994..fefc8e504 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -12,6 +12,7 @@ class SQLiteTest extends Base { public static ?Database $database = null; + protected static ?PDO $pdo = null; protected static string $namespace; // Remove once all methods are implemented @@ -61,6 +62,17 @@ public static function getDatabase(): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = "`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/MariaDBTest.php b/tests/e2e/Adapter/SharedTables/MariaDBTest.php index 249e13319..3cd63e0a7 100644 --- a/tests/e2e/Adapter/SharedTables/MariaDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MariaDBTest.php @@ -13,6 +13,7 @@ class MariaDBTest extends Base { protected static ?Database $database = null; + protected static ?PDO $pdo = null; protected static string $namespace; // Remove once all methods are implemented @@ -59,6 +60,17 @@ public static function getDatabase(bool $fresh = false): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/MongoDBTest.php b/tests/e2e/Adapter/SharedTables/MongoDBTest.php index 47f480073..51027b88f 100644 --- a/tests/e2e/Adapter/SharedTables/MongoDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MongoDBTest.php @@ -98,4 +98,9 @@ public function testKeywords(): void { $this->assertTrue(true); } + + protected static function deleteColumn(string $collection, string $column): bool + { + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/MySQLTest.php b/tests/e2e/Adapter/SharedTables/MySQLTest.php index 689ea49c7..9d3f77c5c 100644 --- a/tests/e2e/Adapter/SharedTables/MySQLTest.php +++ b/tests/e2e/Adapter/SharedTables/MySQLTest.php @@ -13,6 +13,7 @@ class MySQLTest extends Base { public static ?Database $database = null; + protected static ?PDO $pdo = null; protected static string $namespace; // Remove once all methods are implemented @@ -61,6 +62,17 @@ public static function getDatabase(): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/PostgresTest.php b/tests/e2e/Adapter/SharedTables/PostgresTest.php index df85dfeb0..9d108d08a 100644 --- a/tests/e2e/Adapter/SharedTables/PostgresTest.php +++ b/tests/e2e/Adapter/SharedTables/PostgresTest.php @@ -13,6 +13,7 @@ class PostgresTest extends Base { public static ?Database $database = null; + public static ?PDO $pdo = null; protected static string $namespace; /** @@ -58,6 +59,17 @@ public static function getDatabase(): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = '"' . self::getDatabase()->getDatabase() . '"."' . self::getDatabase()->getNamespace() . '_' . $collection . '"'; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN \"{$column}\""; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/SQLiteTest.php b/tests/e2e/Adapter/SharedTables/SQLiteTest.php index a69984216..5ab52a281 100644 --- a/tests/e2e/Adapter/SharedTables/SQLiteTest.php +++ b/tests/e2e/Adapter/SharedTables/SQLiteTest.php @@ -13,6 +13,7 @@ class SQLiteTest extends Base { public static ?Database $database = null; + public static ?PDO $pdo = null; protected static string $namespace; // Remove once all methods are implemented @@ -64,6 +65,17 @@ public static function getDatabase(): Database $database->create(); + self::$pdo = $pdo; return self::$database = $database; } + + protected static function deleteColumn(string $collection, string $column): bool + { + $SQLTable = "`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } From f9321e73637bc1ab270cc4476e82257731d813fa Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 7 Nov 2024 16:07:16 +0900 Subject: [PATCH 145/256] Format and fix PHPStan --- src/Database/Adapter/MariaDB.php | 2 +- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d110b54dc..5271a02c5 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -435,7 +435,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa /** * Rename Attribute * - * @param string $collectionp + * @param string $collection * @param string $old * @param string $new * @return bool diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 105827870..b9dbb796a 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -44,7 +44,7 @@ abstract protected static function getDatabase(): Database; /** * @param string $collection * @param string $column - * + * * @return bool */ abstract protected static function deleteColumn(string $collection, string $column): bool; From 6dde7ef87900024b9e141e37f5014e9ca4a7e913 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 7 Nov 2024 12:21:00 +0200 Subject: [PATCH 146/256] composer --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d0097c5d5..d779556fb 100755 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "ext-mbstring": "*", "php": ">=8.0", "utopia-php/framework": "0.33.*", - "utopia-php/cache": "0.10.*", + "utopia-php/cache": "0.11.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { From 5af02bf3e3970c1cf0272954f6ee485ad42432e9 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 7 Nov 2024 12:32:18 +0200 Subject: [PATCH 147/256] composer --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 1a149c62a..517795e54 100644 --- a/composer.lock +++ b/composer.lock @@ -840,16 +840,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.7", + "version": "1.12.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", "shasum": "" }, "require": { @@ -894,7 +894,7 @@ "type": "github" } ], - "time": "2024-10-18T11:12:07+00:00" + "time": "2024-11-06T19:06:49+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2594,5 +2594,5 @@ "php": ">=8.3" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } From 88b1975c8aed3493f11648621fba3d9a868f069d Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 7 Nov 2024 12:40:20 +0200 Subject: [PATCH 148/256] composer --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 66b0fad6d..a1ca02e4b 100755 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "ext-mbstring": "*", "php": ">=8.3", "utopia-php/framework": "0.33.*", - "utopia-php/cache": "0.11.*", + "utopia-php/cache": "0.10.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 517795e54..3122dd6b5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f7eec4bad737b741ae97c81db0532d29", + "content-hash": "0fffc3b6680e12db596e0fb8dd971606", "packages": [ { "name": "jean85/pretty-package-versions", @@ -216,16 +216,16 @@ }, { "name": "utopia-php/cache", - "version": "0.11.0", + "version": "0.10.2", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc" + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/8ebcab5aac7606331cef69b0081f6c9eff2e58bc", - "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", "shasum": "" }, "require": { @@ -236,7 +236,7 @@ }, "require-dev": { "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.12", + "phpstan/phpstan": "1.9.x-dev", "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.13.1" }, @@ -260,9 +260,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.11.0" + "source": "https://github.com/utopia-php/cache/tree/0.10.2" }, - "time": "2024-11-05T16:53:58+00:00" + "time": "2024-06-25T20:36:35+00:00" }, { "name": "utopia-php/framework", From 9d5faac1bc8bd74f3cba6f6181e559dbaa0e9f89 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 7 Nov 2024 12:43:50 +0200 Subject: [PATCH 149/256] composer --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a1ca02e4b..66b0fad6d 100755 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "ext-mbstring": "*", "php": ">=8.3", "utopia-php/framework": "0.33.*", - "utopia-php/cache": "0.10.*", + "utopia-php/cache": "0.11.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { From 31571bd6adaa9f6bfb31195c49237456b0742ec7 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 7 Nov 2024 12:44:19 +0200 Subject: [PATCH 150/256] composer --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 3122dd6b5..517795e54 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0fffc3b6680e12db596e0fb8dd971606", + "content-hash": "f7eec4bad737b741ae97c81db0532d29", "packages": [ { "name": "jean85/pretty-package-versions", @@ -216,16 +216,16 @@ }, { "name": "utopia-php/cache", - "version": "0.10.2", + "version": "0.11.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" + "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", - "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/8ebcab5aac7606331cef69b0081f6c9eff2e58bc", + "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc", "shasum": "" }, "require": { @@ -236,7 +236,7 @@ }, "require-dev": { "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", + "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.13.1" }, @@ -260,9 +260,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.10.2" + "source": "https://github.com/utopia-php/cache/tree/0.11.0" }, - "time": "2024-06-25T20:36:35+00:00" + "time": "2024-11-05T16:53:58+00:00" }, { "name": "utopia-php/framework", From 529ac35d537e6cc2da2abfa27fd6fe3668934e04 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 8 Nov 2024 13:43:59 +0900 Subject: [PATCH 151/256] Address Comments --- tests/e2e/Adapter/Base.php | 15 +++++++++++++++ tests/e2e/Adapter/MariaDBTest.php | 4 ++-- tests/e2e/Adapter/MySQLTest.php | 4 ++-- tests/e2e/Adapter/PostgresTest.php | 4 ++-- tests/e2e/Adapter/SQLiteTest.php | 4 ++-- tests/e2e/Adapter/SharedTables/MariaDBTest.php | 4 ++-- tests/e2e/Adapter/SharedTables/MySQLTest.php | 4 ++-- tests/e2e/Adapter/SharedTables/PostgresTest.php | 4 ++-- tests/e2e/Adapter/SharedTables/SQLiteTest.php | 4 ++-- 9 files changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b9dbb796a..a7e68e3d3 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1306,8 +1306,23 @@ public function testCreateDeleteAttribute(): void // Test delete attribute when column does not exist $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true)); + sleep(1); + $this->assertEquals(true, static::deleteColumn('attributes', 'string1')); + + $collection = static::getDatabase()->getCollection('attributes'); + $attributes = $collection->getAttribute('attributes'); + $attribute = end($attributes); + $this->assertEquals('string1', $attribute->getId()); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string1')); + + $collection = static::getDatabase()->getCollection('attributes'); + $attributes = $collection->getAttribute('attributes'); + $attribute = end($attributes); + $this->assertNotEquals('string1', $attribute->getId()); + + $collection = static::getDatabase()->getCollection('attributes'); } /** diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index ca5b1988a..10a217d4f 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -63,8 +63,8 @@ public static function getDatabase(bool $fresh = false): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index 467ec820d..1cd3eb2e8 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -65,8 +65,8 @@ public static function getDatabase(): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 6834f9297..ebf2cbc11 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -62,8 +62,8 @@ public static function getDatabase(): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = '"' . self::getDatabase()->getDatabase() . '"."' . self::getDatabase()->getNamespace() . '_' . $collection . '"'; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN \"{$column}\""; + $sqlTable = '"' . self::getDatabase()->getDatabase() . '"."' . self::getDatabase()->getNamespace() . '_' . $collection . '"'; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN \"{$column}\""; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index fefc8e504..0718d5522 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -68,8 +68,8 @@ public static function getDatabase(): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = "`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + $sqlTable = "`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/SharedTables/MariaDBTest.php b/tests/e2e/Adapter/SharedTables/MariaDBTest.php index 3cd63e0a7..f6d58a30f 100644 --- a/tests/e2e/Adapter/SharedTables/MariaDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MariaDBTest.php @@ -66,8 +66,8 @@ public static function getDatabase(bool $fresh = false): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/SharedTables/MySQLTest.php b/tests/e2e/Adapter/SharedTables/MySQLTest.php index 9d3f77c5c..4cd682319 100644 --- a/tests/e2e/Adapter/SharedTables/MySQLTest.php +++ b/tests/e2e/Adapter/SharedTables/MySQLTest.php @@ -68,8 +68,8 @@ public static function getDatabase(): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/SharedTables/PostgresTest.php b/tests/e2e/Adapter/SharedTables/PostgresTest.php index 9d108d08a..23c659958 100644 --- a/tests/e2e/Adapter/SharedTables/PostgresTest.php +++ b/tests/e2e/Adapter/SharedTables/PostgresTest.php @@ -65,8 +65,8 @@ public static function getDatabase(): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = '"' . self::getDatabase()->getDatabase() . '"."' . self::getDatabase()->getNamespace() . '_' . $collection . '"'; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN \"{$column}\""; + $sqlTable = '"' . self::getDatabase()->getDatabase() . '"."' . self::getDatabase()->getNamespace() . '_' . $collection . '"'; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN \"{$column}\""; self::$pdo->exec($sql); diff --git a/tests/e2e/Adapter/SharedTables/SQLiteTest.php b/tests/e2e/Adapter/SharedTables/SQLiteTest.php index 5ab52a281..7cea4fb61 100644 --- a/tests/e2e/Adapter/SharedTables/SQLiteTest.php +++ b/tests/e2e/Adapter/SharedTables/SQLiteTest.php @@ -71,8 +71,8 @@ public static function getDatabase(): Database protected static function deleteColumn(string $collection, string $column): bool { - $SQLTable = "`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; - $sql = "ALTER TABLE {$SQLTable} DROP COLUMN `{$column}`"; + $sqlTable = "`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; self::$pdo->exec($sql); From 10ce467e452bb2426e5bb78744f98a509c98ca05 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 8 Nov 2024 14:28:16 +0900 Subject: [PATCH 152/256] Add missing method in mirror --- tests/e2e/Adapter/MirrorTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php index ee34386f9..40417ab14 100644 --- a/tests/e2e/Adapter/MirrorTest.php +++ b/tests/e2e/Adapter/MirrorTest.php @@ -22,6 +22,7 @@ class MirrorTest extends Base { protected static ?Mirror $database = null; + protected static ?PDO $pdo = null; protected static Database $source; protected static Database $destination; @@ -95,6 +96,7 @@ protected static function getDatabase(bool $fresh = false): Mirror $database->create(); + self::$pdo = $pdo; return self::$database = $database; } @@ -312,4 +314,14 @@ public function testDeleteMirroredDocument(): void $this->assertTrue($database->getSource()->getDocument('testDeleteMirroredDocument', $document->getId())->isEmpty()); $this->assertTrue($database->getDestination()->getDocument('testDeleteMirroredDocument', $document->getId())->isEmpty()); } + + protected static function deleteColumn(string $collection, string $column): bool + { + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; + + self::$pdo->exec($sql); + + return true; + } } From 5c93bd31a021f4154a5275fc90ebbc8dd103782d Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 8 Nov 2024 15:26:52 +0900 Subject: [PATCH 153/256] Address Comments --- tests/e2e/Adapter/MirrorTest.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php index 40417ab14..ae8bd7185 100644 --- a/tests/e2e/Adapter/MirrorTest.php +++ b/tests/e2e/Adapter/MirrorTest.php @@ -22,7 +22,8 @@ class MirrorTest extends Base { protected static ?Mirror $database = null; - protected static ?PDO $pdo = null; + protected static ?PDO $destinationPdo = null; + protected static ?PDO $sourcePdo = null; protected static Database $source; protected static Database $destination; @@ -49,6 +50,7 @@ protected static function getDatabase(bool $fresh = false): Mirror $redis->flushAll(); $cache = new Cache(new RedisAdapter($redis)); + self::$sourcePdo = $pdo; self::$source = new Database(new MariaDB($pdo), $cache); $mirrorHost = 'mariadb-mirror'; @@ -62,6 +64,7 @@ protected static function getDatabase(bool $fresh = false): Mirror $mirrorRedis->flushAll(); $mirrorCache = new Cache(new RedisAdapter($mirrorRedis)); + self::$destinationPdo = $mirrorPdo; self::$destination = new Database(new MariaDB($mirrorPdo), $mirrorCache); $database = new Mirror(self::$source, self::$destination); @@ -96,7 +99,6 @@ protected static function getDatabase(bool $fresh = false): Mirror $database->create(); - self::$pdo = $pdo; return self::$database = $database; } @@ -317,10 +319,15 @@ public function testDeleteMirroredDocument(): void protected static function deleteColumn(string $collection, string $column): bool { - $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sqlTable = "`" . self::$source->getDatabase() . "`.`" . self::$source->getNamespace() . "_" . $collection . "`"; $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; - self::$pdo->exec($sql); + self::$sourcePdo->exec($sql); + + $sqlTable = "`" . self::$destination->getDatabase() . "`.`" . self::$destination->getNamespace() . "_" . $collection . "`"; + $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`"; + + self::$destinationPdo->exec($sql); return true; } From 562f52cc40240efc61f0e7a699eec89ba1f6bbae Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 8 Nov 2024 21:24:31 +1300 Subject: [PATCH 154/256] Fix message --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 4bb23727e..3d43f92f0 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1871,7 +1871,7 @@ public function updateAttribute(string $collection, string $id, string $type = n $this->adapter->getDocumentSizeLimit() > 0 && $this->adapter->getAttributeWidth($collectionDoc) >= $this->adapter->getDocumentSizeLimit() ) { - throw new LimitException('Row width limit reached. Cannot create new attribute.'); + throw new LimitException('Row width limit reached. Cannot update attribute.'); } if ($altering) { From c6ed0114fab9afa39bca170415ace0bc7abcef47 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 10 Nov 2024 12:13:45 +0200 Subject: [PATCH 155/256] Duplicate attributes --- composer.lock | 77 +++++++++++++++++++++++++------- src/Database/Validator/Index.php | 23 +++++++++- tests/e2e/Adapter/Base.php | 42 ++++++++++++++++- 3 files changed, 124 insertions(+), 18 deletions(-) diff --git a/composer.lock b/composer.lock index b4a5e2b5b..d182ba714 100644 --- a/composer.lock +++ b/composer.lock @@ -264,22 +264,69 @@ }, "time": "2024-11-05T16:53:58+00:00" }, + { + "name": "utopia-php/compression", + "version": "0.1.2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/compression.git", + "reference": "6062f70596415f8d5de40a589367b0eb2a435f98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/compression/zipball/6062f70596415f8d5de40a589367b0eb2a435f98", + "reference": "6062f70596415f8d5de40a589367b0eb2a435f98", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Compression\\": "src/Compression" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple Compression library to handle file compression", + "keywords": [ + "compression", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/compression/issues", + "source": "https://github.com/utopia-php/compression/tree/0.1.2" + }, + "time": "2024-11-08T14:59:54+00:00" + }, { "name": "utopia-php/framework", - "version": "0.33.8", + "version": "0.33.11", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" + "reference": "354ff0d23bfc6e82bea0fe8e89e115cff1af8466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", - "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", + "url": "https://api.github.com/repos/utopia-php/http/zipball/354ff0d23bfc6e82bea0fe8e89e115cff1af8466", + "reference": "354ff0d23bfc6e82bea0fe8e89e115cff1af8466", "shasum": "" }, "require": { - "php": ">=8.0" + "php": ">=8.0", + "utopia-php/compression": "0.1.*" }, "require-dev": { "laravel/pint": "^1.2", @@ -305,9 +352,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.8" + "source": "https://github.com/utopia-php/http/tree/0.33.11" }, - "time": "2024-08-15T14:10:09+00:00" + "time": "2024-11-08T18:47:43+00:00" }, { "name": "utopia-php/mongo", @@ -572,16 +619,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -620,7 +667,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -628,7 +675,7 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", @@ -2585,7 +2632,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -2593,6 +2640,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index fdaa1efe0..59a8958d9 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -84,7 +84,9 @@ public function checkDuplicatedAttributes(Document $index): bool $stack = []; foreach ($attributes as $key => $attribute) { $direction = $orders[$key] ?? 'ASC'; - $value = \strtolower($attribute . '|' . $direction); + // Why did we add direction to duplicates id we check only the current Document? + //$value = \strtolower($attribute . '|' . $direction); + $value = \strtolower($attribute); if (\in_array($value, $stack)) { $this->message = 'Duplicate attributes provided'; return false; @@ -209,6 +211,21 @@ public function checkIndexLength(Document $index): bool return true; } + /** + * @param Document $index + * @return bool + */ + public function checkIndexKeyName(Document $index): bool + { + $key = \strtolower($index->getAttribute('key', $index->getAttribute('$id'))); + if ($key === 'primary') { + $this->message = 'Index key name is reserved'; + return false; + } + + return true; + } + /** * Is valid. * @@ -243,6 +260,10 @@ public function isValid($value): bool return false; } + if (!$this->checkIndexKeyName($value)) { + return false; + } + return true; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a94c63dcc..b458f33db 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1097,6 +1097,40 @@ public function testCreateListExistsDeleteCollection(): void $this->assertEquals(false, static::getDatabase()->exists($this->testDatabase, 'actors')); } + public function testCreateIndex() + { + $database = $this->getDatabase(); + + $database->createCollection('indexes'); + $database->createAttribute('indexes', 'name', Database::VAR_STRING, 10, false); + + $database->createIndex('indexes', 'index1', Database::INDEX_KEY, ['name']); + $database->createIndex('indexes', 'index2', Database::INDEX_KEY, ['name']); + + try { + $database->createIndex('indexes', 'primary', Database::INDEX_KEY, ['name']); + } catch (Throwable $e) { + self::assertTrue($e instanceof DatabaseException); + self::assertEquals($e->getMessage(), 'Index key name is reserved'); + } + + try { + $database->createIndex('indexes', 'index3', Database::INDEX_KEY, ['$id', '$id']); + } catch (Throwable $e) { + self::assertTrue($e instanceof DatabaseException); + self::assertEquals($e->getMessage(), 'Duplicate attributes provided'); + } + + try { + $database->createIndex('indexes', 'index4', Database::INDEX_KEY, ['name', 'Name']); + } catch (Throwable $e) { + self::assertTrue($e instanceof DatabaseException); + self::assertEquals($e->getMessage(), 'Duplicate attributes provided'); + } + + $database->deleteCollection('indexes'); + } + public function testSizeCollection(): void { static::getDatabase()->createCollection('sizeTest1'); @@ -1512,8 +1546,12 @@ public function testAttributeNamesWithDots(): void public function testIndexCaseInsensitivity(): void { $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_caseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); - $this->expectException(DuplicateException::class); - $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_CaseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); + + try { + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_CaseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); + } catch (Throwable $e) { + self::assertTrue($e instanceof DuplicateException); + } } /** From 634c07af5f6ef9fc3691010643fc7707158e49b0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 10 Nov 2024 12:22:45 +0200 Subject: [PATCH 156/256] Add void return value --- src/Database/Validator/Index.php | 4 ++-- tests/e2e/Adapter/Base.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 59a8958d9..a6726b455 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -215,7 +215,7 @@ public function checkIndexLength(Document $index): bool * @param Document $index * @return bool */ - public function checkIndexKeyName(Document $index): bool + public function checkReservedNames(Document $index): bool { $key = \strtolower($index->getAttribute('key', $index->getAttribute('$id'))); if ($key === 'primary') { @@ -260,7 +260,7 @@ public function isValid($value): bool return false; } - if (!$this->checkIndexKeyName($value)) { + if (!$this->checkReservedNames($value)) { return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b458f33db..c45206645 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1097,7 +1097,7 @@ public function testCreateListExistsDeleteCollection(): void $this->assertEquals(false, static::getDatabase()->exists($this->testDatabase, 'actors')); } - public function testCreateIndex() + public function testCreateIndex():void { $database = $this->getDatabase(); From 961030713adf64d70839e95393b7bbe906145245 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 10 Nov 2024 12:24:37 +0200 Subject: [PATCH 157/256] lint --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index c45206645..05d896914 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1097,7 +1097,7 @@ public function testCreateListExistsDeleteCollection(): void $this->assertEquals(false, static::getDatabase()->exists($this->testDatabase, 'actors')); } - public function testCreateIndex():void + public function testCreateIndex(): void { $database = $this->getDatabase(); From 4c7127f90452b903abb3daac88fc03d12901099e Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 10 Nov 2024 12:40:06 +0200 Subject: [PATCH 158/256] Double check --- src/Database/Database.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 3d43f92f0..5a3778af2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2747,6 +2747,7 @@ public function createIndex(string $collection, string $id, string $type, array } } catch (DuplicateException $e) { // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. + // Double check this please if (!$this->adapter->getSharedTables()) { throw $e; } From b7b879bd392231678a1d5cfdb5f3bf8115ba3078 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 10 Nov 2024 15:35:10 +0200 Subject: [PATCH 159/256] Fix tests --- tests/e2e/Adapter/Base.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 05d896914..c201ebb9f 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1104,8 +1104,7 @@ public function testCreateIndex(): void $database->createCollection('indexes'); $database->createAttribute('indexes', 'name', Database::VAR_STRING, 10, false); - $database->createIndex('indexes', 'index1', Database::INDEX_KEY, ['name']); - $database->createIndex('indexes', 'index2', Database::INDEX_KEY, ['name']); + $database->createIndex('indexes', 'index_1', Database::INDEX_KEY, ['name']); try { $database->createIndex('indexes', 'primary', Database::INDEX_KEY, ['name']); From a171f5e0abba5966f44d7ba67e35d394bdd5992a Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 10 Nov 2024 15:48:07 +0200 Subject: [PATCH 160/256] Fix unit tests --- tests/unit/Validator/IndexTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index 5464d470a..5606bc343 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -310,6 +310,6 @@ public function testDuplicatedAttributesDifferentOrder(): void $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; - $this->assertTrue($validator->isValid($index)); + $this->assertFalse($validator->isValid($index)); } } From 358aae9242091b080f6c8799abc7f53453665532 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 11 Nov 2024 11:17:43 +0200 Subject: [PATCH 161/256] adapter getIndexKeys --- src/Database/Adapter.php | 7 ++++ src/Database/Adapter/MariaDB.php | 7 +++- src/Database/Adapter/Mongo.php | 5 +++ src/Database/Adapter/SQL.php | 5 +++ src/Database/Database.php | 6 ++-- src/Database/Validator/Index.php | 20 +++++++++--- tests/e2e/Adapter/Base.php | 19 +++++------ tests/unit/Validator/IndexTest.php | 51 ++++++++++++++++++++++++++---- 8 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index b82c4d8de..098b1ec21 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1015,4 +1015,11 @@ abstract public function increaseDocumentAttribute(string $collection, string $i * @return string */ abstract public function getConnectionId(): string; + + /** + * Get List of internal index keys names + * + * @return array + */ + abstract public function getInternalIndexesKeys(): array; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 8bd731594..03d781891 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2425,4 +2425,9 @@ public function getConnectionId(): string $stmt = $this->getPDO()->query("SELECT CONNECTION_ID();"); return $stmt->fetchColumn(); } -} + + public function getInternalIndexesKeys(): array + { + return ['primary']; + } +} \ No newline at end of file diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 44a36f42a..c259ead09 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1937,4 +1937,9 @@ public function getConnectionId(): string { return '0'; } + + public function getInternalIndexesKeys(): array + { + return []; + } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 4e415ad7e..e00b0b83d 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1129,4 +1129,9 @@ public function getLikeOperator(): string return 'LIKE'; } + public function getInternalIndexesKeys(): array + { + return []; + } + } diff --git a/src/Database/Database.php b/src/Database/Database.php index 5a3778af2..58f2b2452 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1167,7 +1167,8 @@ public function createCollection(string $id, array $attributes = [], array $inde if ($this->validate) { $validator = new IndexValidator( $attributes, - $this->adapter->getMaxIndexLength() + $this->adapter->getMaxIndexLength(), + $this->adapter->getInternalIndexesKeys() ); foreach ($indexes as $index) { if (!$validator->isValid($index)) { @@ -2732,7 +2733,8 @@ public function createIndex(string $collection, string $id, string $type, array if ($this->validate) { $validator = new IndexValidator( $collection->getAttribute('attributes', []), - $this->adapter->getMaxIndexLength() + $this->adapter->getMaxIndexLength(), + $this->adapter->getInternalIndexesKeys() ); if (!$validator->isValid($index)) { throw new DatabaseException($validator->getDescription()); diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index a6726b455..2354b8e52 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -17,14 +17,21 @@ class Index extends Validator */ protected array $attributes; + /** + * @var array $reservedKeys + */ + protected array $reservedKeys; + /** * @param array $attributes * @param int $maxLength + * @param array $reservedKeys * @throws DatabaseException */ - public function __construct(array $attributes, int $maxLength) + public function __construct(array $attributes, int $maxLength, array $reservedKeys) { $this->maxLength = $maxLength; + $this->reservedKeys = $reservedKeys; foreach ($attributes as $attribute) { $key = \strtolower($attribute->getAttribute('key', $attribute->getAttribute('$id'))); @@ -217,10 +224,13 @@ public function checkIndexLength(Document $index): bool */ public function checkReservedNames(Document $index): bool { - $key = \strtolower($index->getAttribute('key', $index->getAttribute('$id'))); - if ($key === 'primary') { - $this->message = 'Index key name is reserved'; - return false; + $key = $index->getAttribute('key', $index->getAttribute('$id')); + + foreach ($this->reservedKeys as $reserved) { + if (\strtolower($key) === \strtolower($reserved)) { + $this->message = 'Index key name is reserved'; + return false; + } } return true; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index c201ebb9f..fc70fec42 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -906,7 +906,11 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); - $validator = new Index($attributes, static::getDatabase()->getAdapter()->getMaxIndexLength()); + $validator = new Index( + $attributes, + static::getDatabase()->getAdapter()->getMaxIndexLength(), + static::getDatabase()->getAdapter()->getInternalIndexesKeys() + ); $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; $this->assertFalse($validator->isValid($indexes[0])); @@ -976,7 +980,11 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); - $validator = new Index($attributes, static::getDatabase()->getAdapter()->getMaxIndexLength()); + $validator = new Index( + $attributes, + static::getDatabase()->getAdapter()->getMaxIndexLength(), + static::getDatabase()->getAdapter()->getInternalIndexesKeys() + ); $errorMessage = 'Attribute "integer" cannot be part of a FULLTEXT index, must be of type string'; $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); @@ -1106,13 +1114,6 @@ public function testCreateIndex(): void $database->createIndex('indexes', 'index_1', Database::INDEX_KEY, ['name']); - try { - $database->createIndex('indexes', 'primary', Database::INDEX_KEY, ['name']); - } catch (Throwable $e) { - self::assertTrue($e instanceof DatabaseException); - self::assertEquals($e->getMessage(), 'Index key name is reserved'); - } - try { $database->createIndex('indexes', 'index3', Database::INDEX_KEY, ['$id', '$id']); } catch (Throwable $e) { diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index 5606bc343..c9027bd1b 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -51,7 +51,7 @@ public function testAttributeNotFound(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Invalid index attribute "not_exist" not found', $validator->getDescription()); @@ -100,7 +100,7 @@ public function testFulltextWithNonString(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Attribute "date" cannot be part of a FULLTEXT index, must be of type string', $validator->getDescription()); @@ -138,7 +138,7 @@ public function testIndexLength(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Index length is longer than the maximum: 768', $validator->getDescription()); @@ -185,7 +185,7 @@ public function testMultipleIndexLength(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); $index = $collection->getAttribute('indexes')[0]; $this->assertTrue($validator->isValid($index)); @@ -232,7 +232,7 @@ public function testEmptyAttributes(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('No attributes provided for index', $validator->getDescription()); @@ -270,7 +270,7 @@ public function testDuplicatedAttributes(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Duplicate attributes provided', $validator->getDescription()); @@ -308,7 +308,44 @@ public function testDuplicatedAttributesDifferentOrder(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768); + $validator = new Index($collection->getAttribute('attributes'), 768, []); + $index = $collection->getAttribute('indexes')[0]; + $this->assertFalse($validator->isValid($index)); + } + + /** + * @throws Exception + */ + public function testReservedIndexKey(): void + { + $collection = new Document([ + '$id' => ID::custom('test'), + 'name' => 'test', + 'attributes' => [ + new Document([ + '$id' => ID::custom('title'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ]) + ], + 'indexes' => [ + new Document([ + '$id' => ID::custom('PRIMARY'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['title'], + 'lengths' => [], + 'orders' => [], + ]), + ], + ]); + + $validator = new Index($collection->getAttribute('attributes'), 768, ['PRIMARY']); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); } From 9f6a9309b52a56db294e49df8b8d55f385f30b2a Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 11 Nov 2024 11:22:46 +0200 Subject: [PATCH 162/256] Remove orders check --- src/Database/Validator/Index.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 2354b8e52..0fb905a83 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -87,17 +87,15 @@ public function checkEmptyIndexAttributes(Document $index): bool public function checkDuplicatedAttributes(Document $index): bool { $attributes = $index->getAttribute('attributes', []); - $orders = $index->getAttribute('orders', []); $stack = []; - foreach ($attributes as $key => $attribute) { - $direction = $orders[$key] ?? 'ASC'; - // Why did we add direction to duplicates id we check only the current Document? - //$value = \strtolower($attribute . '|' . $direction); + foreach ($attributes as $attribute) { $value = \strtolower($attribute); + if (\in_array($value, $stack)) { $this->message = 'Duplicate attributes provided'; return false; } + $stack[] = $value; } return true; From 91a0835e6cfe7e26a482c1a75ea4371618d4f364 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 11 Nov 2024 11:37:02 +0200 Subject: [PATCH 163/256] Is migrating --- src/Database/Database.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 58f2b2452..da10e2ce9 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -334,6 +334,8 @@ class Database protected int $maxQueryValues = 100; + protected bool $migrating = false; + /** * Stack of collection IDs when creating or updating related documents * @var array @@ -933,6 +935,18 @@ public function setPreserveDates(bool $preserve): static return $this; } + public function setMigrating(bool $migrating): self + { + $this->migrating = $migrating; + + return $this; + } + + public function isMigrating(): bool + { + return $this->migrating; + } + public function withPreserveDates(callable $callback): mixed { $previous = $this->preserveDates; @@ -2749,10 +2763,11 @@ public function createIndex(string $collection, string $id, string $type, array } } catch (DuplicateException $e) { // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. - // Double check this please - if (!$this->adapter->getSharedTables()) { + + if(!$this->adapter->getSharedTables() || !$this->isMigrating()){ throw $e; } + } if ($collection->getId() !== self::METADATA) { From dbe87e773b442b57185df2a33a83a73f3a576446 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 11 Nov 2024 11:39:14 +0200 Subject: [PATCH 164/256] Line feed --- src/Database/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index da10e2ce9..85dc24e99 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2767,7 +2767,6 @@ public function createIndex(string $collection, string $id, string $type, array if(!$this->adapter->getSharedTables() || !$this->isMigrating()){ throw $e; } - } if ($collection->getId() !== self::METADATA) { From 2c01fead4e94713ce7621956acaab448f3937192 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 11 Nov 2024 11:44:24 +0200 Subject: [PATCH 165/256] isMigrating --- src/Database/Adapter/MariaDB.php | 4 ++-- src/Database/Adapter/SQL.php | 1 - src/Database/Database.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 03d781891..0d1aa7a94 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2428,6 +2428,6 @@ public function getConnectionId(): string public function getInternalIndexesKeys(): array { - return ['primary']; + return ['primary', '_created_at', '_updated_at', '_tenant_id']; } -} \ No newline at end of file +} diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index e00b0b83d..900e39699 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1133,5 +1133,4 @@ public function getInternalIndexesKeys(): array { return []; } - } diff --git a/src/Database/Database.php b/src/Database/Database.php index 85dc24e99..80535c7b5 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2764,7 +2764,7 @@ public function createIndex(string $collection, string $id, string $type, array } catch (DuplicateException $e) { // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. - if(!$this->adapter->getSharedTables() || !$this->isMigrating()){ + if (!$this->adapter->getSharedTables() || !$this->isMigrating()) { throw $e; } } From 82acc1c3f1543e994c7d433b19d6ded289a175e8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 11 Nov 2024 13:48:03 +0200 Subject: [PATCH 166/256] phpstan --- src/Database/Adapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 098b1ec21..ae6627f97 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1019,7 +1019,7 @@ abstract public function getConnectionId(): string; /** * Get List of internal index keys names * - * @return array + * @return array */ abstract public function getInternalIndexesKeys(): array; } From 075ab1d41fd9fdbbcfbaaa7d659367bc2704f4fe Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 12 Nov 2024 13:07:51 +1300 Subject: [PATCH 167/256] Update create attribute logic --- src/Database/Database.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 80535c7b5..8f47cf831 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1527,10 +1527,17 @@ public function createAttribute(string $collection, string $id, string $type, in $this->validateDefaultTypes($type, $default); } - $created = $this->adapter->createAttribute($collection->getId(), $id, $type, $size, $signed, $array); + try { + $created = $this->adapter->createAttribute($collection->getId(), $id, $type, $size, $signed, $array); - if (!$created) { - throw new DatabaseException('Failed to create attribute'); + if (!$created) { + throw new DatabaseException('Failed to create attribute'); + } + } catch (DuplicateException $e) { + // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. + if (!$this->adapter->getSharedTables() || !$this->isMigrating()) { + throw $e; + } } if ($collection->getId() !== self::METADATA) { From b0126cc936833d1094b0470037ef8a5200c17ae8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 12 Nov 2024 13:13:12 +1300 Subject: [PATCH 168/256] Add default param --- Dockerfile | 3 --- src/Database/Validator/Index.php | 2 +- tests/unit/Validator/IndexTest.php | 16 ++++++++-------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index f7985eba1..405656e47 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,5 @@ FROM composer:2.0 AS composer -ARG DEBUG=false -ENV DEBUG=$DEBUG - WORKDIR /usr/local/src/ COPY composer.lock /usr/local/src/ diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 0fb905a83..2270a6e0f 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -28,7 +28,7 @@ class Index extends Validator * @param array $reservedKeys * @throws DatabaseException */ - public function __construct(array $attributes, int $maxLength, array $reservedKeys) + public function __construct(array $attributes, int $maxLength, array $reservedKeys = []) { $this->maxLength = $maxLength; $this->reservedKeys = $reservedKeys; diff --git a/tests/unit/Validator/IndexTest.php b/tests/unit/Validator/IndexTest.php index c9027bd1b..a2862830c 100644 --- a/tests/unit/Validator/IndexTest.php +++ b/tests/unit/Validator/IndexTest.php @@ -51,7 +51,7 @@ public function testAttributeNotFound(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Invalid index attribute "not_exist" not found', $validator->getDescription()); @@ -100,7 +100,7 @@ public function testFulltextWithNonString(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Attribute "date" cannot be part of a FULLTEXT index, must be of type string', $validator->getDescription()); @@ -138,7 +138,7 @@ public function testIndexLength(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Index length is longer than the maximum: 768', $validator->getDescription()); @@ -185,7 +185,7 @@ public function testMultipleIndexLength(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertTrue($validator->isValid($index)); @@ -232,7 +232,7 @@ public function testEmptyAttributes(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('No attributes provided for index', $validator->getDescription()); @@ -270,7 +270,7 @@ public function testDuplicatedAttributes(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); $this->assertEquals('Duplicate attributes provided', $validator->getDescription()); @@ -308,7 +308,7 @@ public function testDuplicatedAttributesDifferentOrder(): void ], ]); - $validator = new Index($collection->getAttribute('attributes'), 768, []); + $validator = new Index($collection->getAttribute('attributes'), 768); $index = $collection->getAttribute('indexes')[0]; $this->assertFalse($validator->isValid($index)); } @@ -336,7 +336,7 @@ public function testReservedIndexKey(): void ], 'indexes' => [ new Document([ - '$id' => ID::custom('PRIMARY'), + '$id' => ID::custom('primary'), 'type' => Database::INDEX_FULLTEXT, 'attributes' => ['title'], 'lengths' => [], From 285bbfc623f67d8ba3cc2b9607ee16b94506def0 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Tue, 12 Nov 2024 16:08:47 +0900 Subject: [PATCH 169/256] Update Bulk Document events to use document --- src/Database/Database.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 3d43f92f0..6438407aa 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4047,7 +4047,10 @@ public function updateDocuments(string $collection, Document $updates, array $qu $lastDocument = end($affectedDocuments); } - $this->trigger(self::EVENT_DOCUMENTS_UPDATE, $affectedDocumentIds); + $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ + '$collection' => $collection->getId(), + '$ids' => $affectedDocumentIds, + ])); foreach ($affectedDocumentIds as $id) { $this->purgeRelatedDocuments($collection, $id); @@ -5195,7 +5198,10 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba return 0; } - $this->trigger(self::EVENT_DOCUMENTS_DELETE, $affectedDocumentIds); + $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ + '$collection' => $collection->getId(), + '$ids' => $affectedDocumentIds + ])); // Mass delete using adapter with query return $this->adapter->deleteDocuments($collection->getId(), $affectedDocumentIds); From 692eeee5d54efe0a1386a4ec8ed99fad25e8b371 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Tue, 12 Nov 2024 17:24:27 +0900 Subject: [PATCH 170/256] Add tests fix silent events --- src/Database/Database.php | 17 +++++++++-------- tests/e2e/Adapter/Base.php | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 6438407aa..b16d08500 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3979,10 +3979,11 @@ public function updateDocuments(string $collection, Document $updates, array $qu throw new StructureException($validator->getDescription()); } - $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor) { + $affectedDocumentIds = []; + + $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor, $affectedDocumentIds) { $lastDocument = null; $totalModified = 0; - $affectedDocumentIds = []; $documentSecurity = $collection->getAttribute('documentSecurity', false); @@ -4004,7 +4005,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu $limit -= $batchSize; } - $affectedDocuments = $this->find($collection->getId(), array_merge( + $affectedDocuments = $this->silent(fn () => $this->find($collection->getId(), array_merge( $queries, empty($lastDocument) ? [ Query::limit($batchSize), @@ -4012,7 +4013,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu Query::limit($batchSize), Query::cursorAfter($lastDocument), ] - ), forPermission: Database::PERMISSION_UPDATE); + ), forPermission: Database::PERMISSION_UPDATE)); if (empty($affectedDocuments)) { break; @@ -4049,7 +4050,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ '$collection' => $collection->getId(), - '$ids' => $affectedDocumentIds, + 'modified' => $affectedDocumentIds, ])); foreach ($affectedDocumentIds as $id) { @@ -5159,7 +5160,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $limit -= $batchSize; } - $affectedDocuments = $this->find($collection->getId(), array_merge( + $affectedDocuments = $this->silent(fn () => $this->find($collection->getId(), array_merge( $queries, empty($lastDocument) ? [ Query::limit($batchSize), @@ -5167,7 +5168,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba Query::limit($batchSize), Query::cursorAfter($lastDocument), ] - ), forPermission: Database::PERMISSION_DELETE); + ), forPermission: Database::PERMISSION_DELETE)); if (empty($affectedDocuments)) { break; @@ -5200,7 +5201,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ '$collection' => $collection->getId(), - '$ids' => $affectedDocumentIds + 'modified' => $affectedDocumentIds ])); // Mass delete using adapter with query diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a94c63dcc..44a7e614e 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -16922,11 +16922,13 @@ public function testEvents(): void Database::EVENT_DOCUMENT_SUM, Database::EVENT_DOCUMENT_INCREASE, Database::EVENT_DOCUMENT_DECREASE, + Database::EVENT_DOCUMENTS_CREATE, + Database::EVENT_DOCUMENTS_UPDATE, Database::EVENT_INDEX_DELETE, Database::EVENT_DOCUMENT_DELETE, Database::EVENT_ATTRIBUTE_DELETE, Database::EVENT_COLLECTION_DELETE, - Database::EVENT_DATABASE_DELETE, + Database::EVENT_DATABASE_DELETE ]; $database->on(Database::EVENT_ALL, 'test', function ($event, $data) use (&$events) { @@ -16983,8 +16985,22 @@ public function testEvents(): void $this->assertFalse($executed); + $database->createDocuments($collectionId, [ + new Document([ + 'attr1' => 10, + ]), + new Document([ + 'attr1' => 20, + ]), + ]); + + $database->updateDocuments($collectionId, new Document([ + 'attr1' => 15, + ])); + $database->deleteIndex($collectionId, $indexId1); $database->deleteDocument($collectionId, 'doc1'); + $database->deleteDocuments($collectionId); $database->deleteAttribute($collectionId, 'attr1'); $database->deleteCollection($collectionId); $database->delete('hellodb'); From 22af097dc245f428bf5d180c7f7f809f041ace58 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Tue, 12 Nov 2024 17:33:38 +0900 Subject: [PATCH 171/256] Fix missing test --- tests/e2e/Adapter/Base.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 44a7e614e..d30bc1f61 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -16926,6 +16926,7 @@ public function testEvents(): void Database::EVENT_DOCUMENTS_UPDATE, Database::EVENT_INDEX_DELETE, Database::EVENT_DOCUMENT_DELETE, + Database::EVENT_DOCUMENTS_DELETE, Database::EVENT_ATTRIBUTE_DELETE, Database::EVENT_COLLECTION_DELETE, Database::EVENT_DATABASE_DELETE From e47e5c597d4dd6692f30121c82ec0d23e10a0de9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 12 Nov 2024 11:27:39 +0200 Subject: [PATCH 172/256] add test --- composer.lock | 10 +++++----- phpunit.xml | 2 +- src/Database/Adapter/MariaDB.php | 2 +- tests/e2e/Adapter/Base.php | 10 ++++++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index d182ba714..afb62bcd1 100644 --- a/composer.lock +++ b/composer.lock @@ -887,16 +887,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.8", + "version": "1.12.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", - "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", "shasum": "" }, "require": { @@ -941,7 +941,7 @@ "type": "github" } ], - "time": "2024-11-06T19:06:49+00:00" + "time": "2024-11-11T15:37:09+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/phpunit.xml b/phpunit.xml index ccdaa969e..783265d80 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/unit diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0d1aa7a94..154c12ea2 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -406,7 +406,7 @@ public function updateAttribute(string $collection, string $id, string $type, in } else { $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; } - +var_dump($sql); $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); try { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index fc70fec42..93c1f5c2a 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6203,6 +6203,16 @@ public function testUpdateAttributeSize(): void $this->fail('Succeeded updating attribute size to smaller size with data that is too big'); } catch (TruncateException $e) { } + + $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'attr1', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'attr2', Database::VAR_STRING, 128, true)); + + static::getDatabase()->createIndex('resize_test', 'index', Database::INDEX_KEY, ['attr1', 'attr2'], [128, 128]); + $this->assertEquals('shmuel', 'fogel'); + + static::getDatabase()->updateAttribute('resize_test', 'attr1', Database::VAR_STRING, 5000); + + $this->assertEquals('shmuel', 'fogel'); } /** From d909e760f25758c3e3590729ae78d41aa2159b2d Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 12 Nov 2024 11:55:15 +0200 Subject: [PATCH 173/256] sharedTables index --- src/Database/Adapter/MariaDB.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 0d1aa7a94..84889d15d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -164,9 +164,9 @@ public function createCollection(string $name, array $attributes = [], array $in $collection .= " _tenant INT(11) UNSIGNED DEFAULT NULL, UNIQUE KEY _uid (_uid, _tenant), - KEY _created_at (_createdAt, _tenant), - KEY _updated_at (_updatedAt, _tenant), - KEY _tenant_id (_id, _tenant) + KEY _created_at (_tenant, _createdAt), + KEY _updated_at (_tenant, _updatedAt), + KEY _tenant_id (_tenant, _id) "; } else { $collection .= " From 7f264cb450c441fa1e8e8eb7314a68ac10b3b397 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 12 Nov 2024 11:59:04 +0200 Subject: [PATCH 174/256] Postgress --- src/Database/Adapter/Postgres.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 116807a67..d47478c12 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -215,9 +215,9 @@ public function createCollection(string $name, array $attributes = [], array $in if ($this->sharedTables) { $collection .= " 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)} (\"_createdAt\", _tenant); - CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\", _tenant); - CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_tenant_id\" ON {$this->getSQLTable($id)} (_id, _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 { $collection .= " From ef1d903672359cb63fe6ade6f2a60c76867a7a67 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:15:06 +0200 Subject: [PATCH 175/256] Unset length --- src/Database/Adapter/MariaDB.php | 2 + src/Database/Adapter/SQL.php | 5 ++- src/Database/Database.php | 36 +++++++++++++++-- tests/e2e/Adapter/Base.php | 68 ++++++++++++++++++++++++++++---- 4 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 193ec594d..d2ca25d47 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -794,6 +794,8 @@ public function createIndex(string $collection, string $id, string $type, array } $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection->getId())} ({$attributes})"; + + var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); try { diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 900e39699..48b52227f 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1085,7 +1085,10 @@ public function getMaxVarcharLength(): int */ public function getMaxIndexLength(): int { - return 768; + /** + * $tenant int = 1 + */ + return $this->sharedTables ? 767:768; } /** diff --git a/src/Database/Database.php b/src/Database/Database.php index 8f47cf831..79b3a84c6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1897,11 +1897,9 @@ public function updateAttribute(string $collection, string $id, string $type = n } if ($altering) { - $updated = $this->adapter->updateAttribute($collection, $id, $type, $size, $signed, $array, $newKey); - - if ($id !== $newKey) { - $indexes = $collectionDoc->getAttribute('indexes'); + $indexes = $collectionDoc->getAttribute('indexes'); + if (!\is_null($newKey) && $id !== $newKey) { foreach ($indexes as $index) { if (in_array($id, $index['attributes'])) { $index['attributes'] = array_map(function ($attribute) use ($id, $newKey) { @@ -1911,6 +1909,25 @@ public function updateAttribute(string $collection, string $id, string $type = n } } + /** + * Since we allow changing type & size we need to validate index length + */ + if ($this->validate) { + $validator = new IndexValidator( + $attributes, + $this->adapter->getMaxIndexLength(), + $this->adapter->getInternalIndexesKeys() + ); + + foreach ($indexes as $index) { + if (!$validator->isValid($index)) { + throw new DatabaseException($validator->getDescription()); + } + } + } + + $updated = $this->adapter->updateAttribute($collection, $id, $type, $size, $signed, $array, $newKey); + if (!$updated) { throw new DatabaseException('Failed to update attribute'); } @@ -2728,6 +2745,16 @@ public function createIndex(string $collection, string $id, string $type, array foreach ($attributes as $i => $attr) { foreach ($collectionAttributes as $collectionAttribute) { if ($collectionAttribute->getAttribute('key') === $attr) { + + /** + * mysql does not save length in collection when length = attributes size + */ + if ($collectionAttribute->getAttribute('type') === Database::VAR_STRING) { + if(!empty($lengths[$i]) && $lengths[$i] === $collectionAttribute->getAttribute('size') && $this->adapter->getMaxIndexLength() > 0){ + $lengths[$i] = null; + } + } + $isArray = $collectionAttribute->getAttribute('array', false); if ($isArray) { if ($this->adapter->getMaxIndexLength() > 0) { @@ -2740,6 +2767,7 @@ public function createIndex(string $collection, string $id, string $type, array } } + \var_dump($orders); $index = new Document([ '$id' => ID::custom($id), 'key' => $id, diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 93c1f5c2a..0cbd6e436 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2844,7 +2844,7 @@ public function testArrayAttribute(): void $database->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] $this->fail('Failed to throw exception'); } catch (Throwable $e) { - $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); + $this->assertEquals('Index length is longer than the maximum: ' . $database->getAdapter()->getMaxIndexLength(), $e->getMessage()); } } @@ -6010,14 +6010,22 @@ public function testUpdateAttributeRename(): void $collection = static::getDatabase()->getCollection('rename_test'); $this->assertEquals('renamed', $collection->getAttribute('attributes')[0]['key']); $this->assertEquals('renamed', $collection->getAttribute('attributes')[0]['$id']); + $this->assertEquals('renamed', $collection->getAttribute('indexes')[0]['attributes'][0]); - // Check empty key doesn't cause issues + // Check empty newKey doesn't cause issues static::getDatabase()->updateAttribute( collection: 'rename_test', id: 'renamed', type: Database::VAR_STRING, ); + //shmuel + $collection = static::getDatabase()->getCollection('rename_test'); + + $this->assertEquals('renamed', $collection->getAttribute('attributes')[0]['key']); + $this->assertEquals('renamed', $collection->getAttribute('attributes')[0]['$id']); + $this->assertEquals('renamed', $collection->getAttribute('indexes')[0]['attributes'][0]); + $doc = static::getDatabase()->getDocument('rename_test', $doc->getId()); $this->assertEquals('string2', $doc->getAttribute('renamed')); @@ -6204,15 +6212,59 @@ public function testUpdateAttributeSize(): void } catch (TruncateException $e) { } - $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'attr1', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'attr2', Database::VAR_STRING, 128, true)); + if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { + $length = intval(static::getDatabase()->getAdapter()->getMaxIndexLength() / 2); - static::getDatabase()->createIndex('resize_test', 'index', Database::INDEX_KEY, ['attr1', 'attr2'], [128, 128]); - $this->assertEquals('shmuel', 'fogel'); + $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'attr1', Database::VAR_STRING, $length, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('resize_test', 'attr2', Database::VAR_STRING, $length, true)); - static::getDatabase()->updateAttribute('resize_test', 'attr1', Database::VAR_STRING, 5000); + /** + * No index length provided, we are able to validate + */ + static::getDatabase()->createIndex('resize_test', 'index1', Database::INDEX_KEY, ['attr1', 'attr2']); - $this->assertEquals('shmuel', 'fogel'); + try { + static::getDatabase()->updateAttribute('resize_test', 'attr1', Database::VAR_STRING, 5000); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertEquals('Index length is longer than the maximum: '.static::getDatabase()->getAdapter()->getMaxIndexLength(), $e->getMessage()); + } + + static::getDatabase()->deleteIndex('resize_test', 'index1'); + + /** + * Index lengths are provided, We are able to validate + * Index $length === attr1, $length === attr2, so $length is removed, so we are able to validate + */ + static::getDatabase()->createIndex('resize_test', 'index1', Database::INDEX_KEY, ['attr1', 'attr2'], [$length, $length]); + + $collection = static::getDatabase()->getCollection('resize_test'); + $indexes = $collection->getAttribute('indexes', []); + $this->assertEquals(null, $indexes[0]['lengths'][0]); + $this->assertEquals(null, $indexes[0]['lengths'][1]); + + try { + static::getDatabase()->updateAttribute('resize_test', 'attr1', Database::VAR_STRING, 5000); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertEquals('Index length is longer than the maximum: '.static::getDatabase()->getAdapter()->getMaxIndexLength(), $e->getMessage()); + } + + static::getDatabase()->deleteIndex('resize_test', 'index1'); + + /** + * Index lengths are provided + * We are able to increase size because index length remains 50 + */ + static::getDatabase()->createIndex('resize_test', 'index1', Database::INDEX_KEY, ['attr1', 'attr2'], [50, 50]); + + $collection = static::getDatabase()->getCollection('resize_test'); + $indexes = $collection->getAttribute('indexes', []); + $this->assertEquals(50, $indexes[0]['lengths'][0]); + $this->assertEquals(50, $indexes[0]['lengths'][1]); + + static::getDatabase()->updateAttribute('resize_test', 'attr1', Database::VAR_STRING, 5000); + } } /** From c630f34e6073d0ba291dc8fd10a63dbe99638b8d Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:16:31 +0200 Subject: [PATCH 176/256] remove comments --- src/Database/Adapter/MariaDB.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d2ca25d47..08142f0e3 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -406,7 +406,7 @@ public function updateAttribute(string $collection, string $id, string $type, in } else { $sql = "ALTER TABLE {$this->getSQLTable($name)} MODIFY `{$id}` {$type};"; } -var_dump($sql); + $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); try { From a5623f9c5c94d9ba278dd6e5134fe8df9eb620c2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:17:40 +0200 Subject: [PATCH 177/256] remove comments --- src/Database/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 79b3a84c6..3479bc295 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2767,7 +2767,6 @@ public function createIndex(string $collection, string $id, string $type, array } } - \var_dump($orders); $index = new Document([ '$id' => ID::custom($id), 'key' => $id, From 0e1bf186c837a4825a50ac22d9d3e0ae78a56447 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:19:29 +0200 Subject: [PATCH 178/256] stopOnFailure --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 783265d80..ccdaa969e 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true"> + stopOnFailure="false"> ./tests/unit From 73f9e5fb408c90102cbe4fa4b559890c07e00d66 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:20:53 +0200 Subject: [PATCH 179/256] remove var_dump --- src/Database/Adapter/MariaDB.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 08142f0e3..1ce0aad62 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -795,7 +795,6 @@ public function createIndex(string $collection, string $id, string $type, array $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection->getId())} ({$attributes})"; - var_dump($sql); $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); try { From 1a4020e751f65cf32a3a6d7968efd9026419703d Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:22:54 +0200 Subject: [PATCH 180/256] formatting --- src/Database/Adapter/MariaDB.php | 1 - src/Database/Adapter/SQL.php | 2 +- src/Database/Database.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 1ce0aad62..84889d15d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -794,7 +794,6 @@ public function createIndex(string $collection, string $id, string $type, array } $sql = "CREATE {$sqlType} `{$id}` ON {$this->getSQLTable($collection->getId())} ({$attributes})"; - $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); try { diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 48b52227f..7e649919d 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1088,7 +1088,7 @@ public function getMaxIndexLength(): int /** * $tenant int = 1 */ - return $this->sharedTables ? 767:768; + return $this->sharedTables ? 767 : 768; } /** diff --git a/src/Database/Database.php b/src/Database/Database.php index 3479bc295..2c994b4f0 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2750,7 +2750,7 @@ public function createIndex(string $collection, string $id, string $type, array * mysql does not save length in collection when length = attributes size */ if ($collectionAttribute->getAttribute('type') === Database::VAR_STRING) { - if(!empty($lengths[$i]) && $lengths[$i] === $collectionAttribute->getAttribute('size') && $this->adapter->getMaxIndexLength() > 0){ + if (!empty($lengths[$i]) && $lengths[$i] === $collectionAttribute->getAttribute('size') && $this->adapter->getMaxIndexLength() > 0) { $lengths[$i] = null; } } From 5c50d1f848bceda1762326e86a3dd9a7b3379879 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 17:37:30 +0200 Subject: [PATCH 181/256] revert lock --- composer.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index afb62bcd1..d182ba714 100644 --- a/composer.lock +++ b/composer.lock @@ -887,16 +887,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.10", + "version": "1.12.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", - "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", "shasum": "" }, "require": { @@ -941,7 +941,7 @@ "type": "github" } ], - "time": "2024-11-11T15:37:09+00:00" + "time": "2024-11-06T19:06:49+00:00" }, { "name": "phpunit/php-code-coverage", From 64ba2e98935f1bb459074f1923f1b8dda2d91e1c Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 18:00:22 +0200 Subject: [PATCH 182/256] lock --- composer.lock | 2056 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1886 insertions(+), 170 deletions(-) diff --git a/composer.lock b/composer.lock index d182ba714..3151d0c17 100644 --- a/composer.lock +++ b/composer.lock @@ -6,6 +6,191 @@ ], "content-hash": "f7eec4bad737b741ae97c81db0532d29", "packages": [ + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "google/protobuf", + "version": "v4.28.3", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c5c311e0f3d89928251ac5a2f0e3db283612c100", + "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": ">=5.0.0" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.28.3" + }, + "time": "2024-10-22T22:27:17+00:00" + }, { "name": "jean85/pretty-package-versions", "version": "2.0.6", @@ -116,57 +301,1607 @@ "email": "andreas.braun@mongodb.com" }, { - "name": "Jeremy Mikola", - "email": "jmikola@gmail.com" + "name": "Jeremy Mikola", + "email": "jmikola@gmail.com" + } + ], + "description": "MongoDB driver library", + "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "keywords": [ + "database", + "driver", + "mongodb", + "persistence" + ], + "support": { + "issues": "https://github.com/mongodb/mongo-php-library/issues", + "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" + }, + "time": "2021-10-20T22:22:37+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-08T09:30:43+00:00" + }, + { + "name": "open-telemetry/api", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/542064815d38a6df55af7957cd6f1d7d967c99c6", + "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6", + "shasum": "" + }, + "require": { + "open-telemetry/context": "^1.0", + "php": "^8.1", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/polyfill-php82": "^1.26" + }, + "conflict": { + "open-telemetry/sdk": "<=1.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.1.x-dev" + }, + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + } + }, + "autoload": { + "files": [ + "Trace/functions.php" + ], + "psr-4": { + "OpenTelemetry\\API\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "API for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "api", + "apm", + "logging", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-10-15T22:42:37+00:00" + }, + { + "name": "open-telemetry/context", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/context.git", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-php82": "^1.26" + }, + "suggest": { + "ext-ffi": "To allow context switching in Fibers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "fiber/initialize_fiber_handler.php" + ], + "psr-4": { + "OpenTelemetry\\Context\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Context implementation for OpenTelemetry PHP.", + "keywords": [ + "Context", + "opentelemetry", + "otel" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-08-21T00:29:20+00:00" + }, + { + "name": "open-telemetry/exporter-otlp", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/exporter-otlp.git", + "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/9b6de12204f25f8ab9540b46d6e7b5151897ce18", + "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18", + "shasum": "" + }, + "require": { + "open-telemetry/api": "^1.0", + "open-telemetry/gen-otlp-protobuf": "^1.1", + "open-telemetry/sdk": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Otlp\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "OTLP exporter for OpenTelemetry.", + "keywords": [ + "Metrics", + "exporter", + "gRPC", + "http", + "opentelemetry", + "otel", + "otlp", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-04-30T18:28:30+00:00" + }, + { + "name": "open-telemetry/gen-otlp-protobuf", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", + "reference": "66c3b98e998a726691c92e6405a82e6e7b8b169d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/66c3b98e998a726691c92e6405a82e6e7b8b169d", + "reference": "66c3b98e998a726691c92e6405a82e6e7b8b169d", + "shasum": "" + }, + "require": { + "google/protobuf": "^3.22 || ^4.0", + "php": "^8.0" + }, + "suggest": { + "ext-protobuf": "For better performance, when dealing with the protobuf format" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opentelemetry\\Proto\\": "Opentelemetry/Proto/", + "GPBMetadata\\Opentelemetry\\": "GPBMetadata/Opentelemetry/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "PHP protobuf files for communication with OpenTelemetry OTLP collectors/servers.", + "keywords": [ + "Metrics", + "apm", + "gRPC", + "logging", + "opentelemetry", + "otel", + "otlp", + "protobuf", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-10-30T11:49:49+00:00" + }, + { + "name": "open-telemetry/sdk", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sdk.git", + "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/fb0ff8d8279a3776bd604791e2531dd0cc147e8b", + "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nyholm/psr7-server": "^1.1", + "open-telemetry/api": "~1.0 || ~1.1", + "open-telemetry/context": "^1.0", + "open-telemetry/sem-conv": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0.1|^2.0", + "psr/log": "^1.1|^2.0|^3.0", + "ramsey/uuid": "^3.0 || ^4.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php82": "^1.26", + "tbachert/spi": "^1.0.1" + }, + "suggest": { + "ext-gmp": "To support unlimited number of synchronous metric readers", + "ext-mbstring": "To increase performance of string operations", + "open-telemetry/sdk-configuration": "File-based OpenTelemetry SDK configuration" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + }, + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + } + }, + "autoload": { + "files": [ + "Common/Util/functions.php", + "Logs/Exporter/_register.php", + "Metrics/MetricExporter/_register.php", + "Propagation/_register.php", + "Trace/SpanExporter/_register.php", + "Common/Dev/Compatibility/_load.php", + "_autoload.php" + ], + "psr-4": { + "OpenTelemetry\\SDK\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "SDK for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "sdk", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-10-18T21:01:35+00:00" + }, + { + "name": "open-telemetry/sem-conv", + "version": "1.27.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sem-conv.git", + "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/1dba705fea74bc0718d04be26090e3697e56f4e6", + "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\SemConv\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Semantic conventions for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "semantic conventions", + "semconv", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-08-28T09:20:31+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", + "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.1.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:40:27+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "20414d96f391677bf80078aa55baece78b82647d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "MongoDB driver library", - "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "database", - "driver", - "mongodb", - "persistence" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.31.0" }, - "time": "2021-10-20T22:22:37+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "name": "symfony/service-contracts", + "version": "v3.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Contracts\\Service\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -174,10 +1909,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -187,16 +1918,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Generic abstractions related to writing services", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -212,7 +1945,59 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "tbachert/spi", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Nevay/spi.git", + "reference": "2ddfaf815dafb45791a61b08170de8d583c16062" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062", + "reference": "2ddfaf815dafb45791a61b08170de8d583c16062", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "composer/semver": "^1.0 || ^2.0 || ^3.0", + "php": "^8.1" + }, + "require-dev": { + "composer/composer": "^2.0", + "infection/infection": "^0.27.9", + "phpunit/phpunit": "^10.5", + "psalm/phar": "^5.18" + }, + "type": "composer-plugin", + "extra": { + "branch-alias": { + "dev-main": "0.2.x-dev" + }, + "class": "Nevay\\SPI\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Nevay\\SPI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Service provider loading facility", + "keywords": [ + "service provider" + ], + "support": { + "issues": "https://github.com/Nevay/spi/issues", + "source": "https://github.com/Nevay/spi/tree/v1.0.2" + }, + "time": "2024-10-04T16:36:12+00:00" }, { "name": "utopia-php/cache", @@ -312,21 +2097,22 @@ }, { "name": "utopia-php/framework", - "version": "0.33.11", + "version": "0.33.12", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "354ff0d23bfc6e82bea0fe8e89e115cff1af8466" + "reference": "bfb7812df9e489b3cba7d5504a49ce578c71af1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/354ff0d23bfc6e82bea0fe8e89e115cff1af8466", - "reference": "354ff0d23bfc6e82bea0fe8e89e115cff1af8466", + "url": "https://api.github.com/repos/utopia-php/http/zipball/bfb7812df9e489b3cba7d5504a49ce578c71af1f", + "reference": "bfb7812df9e489b3cba7d5504a49ce578c71af1f", "shasum": "" }, "require": { - "php": ">=8.0", - "utopia-php/compression": "0.1.*" + "php": ">=8.1", + "utopia-php/compression": "0.1.*", + "utopia-php/telemetry": "0.1.*" }, "require-dev": { "laravel/pint": "^1.2", @@ -352,9 +2138,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.11" + "source": "https://github.com/utopia-php/http/tree/0.33.12" }, - "time": "2024-11-08T18:47:43+00:00" + "time": "2024-11-13T12:45:45+00:00" }, { "name": "utopia-php/mongo", @@ -415,6 +2201,56 @@ "source": "https://github.com/utopia-php/mongo/tree/0.3.1" }, "time": "2023-09-01T17:25:28+00:00" + }, + { + "name": "utopia-php/telemetry", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/telemetry.git", + "reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/d35f2f0632f4ee0be63fb7ace6a94a6adda71a80", + "reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80", + "shasum": "" + }, + "require": { + "ext-opentelemetry": "*", + "ext-protobuf": "*", + "nyholm/psr7": "^1.8", + "open-telemetry/exporter-otlp": "^1.1", + "open-telemetry/sdk": "^1.1", + "php": ">=8.0", + "symfony/http-client": "^7.1" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/telemetry/issues", + "source": "https://github.com/utopia-php/telemetry/tree/0.1.0" + }, + "time": "2024-11-13T10:29:53+00:00" } ], "packages-dev": [ @@ -887,16 +2723,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.8", + "version": "1.12.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", - "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", "shasum": "" }, "require": { @@ -941,7 +2777,7 @@ "type": "github" } ], - "time": "2024-11-06T19:06:49+00:00" + "time": "2024-11-11T15:37:09+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1365,59 +3201,6 @@ ], "time": "2024-09-19T10:50:18+00:00" }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, { "name": "rregeer/phpunit-coverage-check", "version": "0.3.1", @@ -2459,73 +4242,6 @@ }, "time": "2024-06-17T05:45:20+00:00" }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", From 4d99a51600b13cb866afa0857c50aa09c7c70828 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 13 Nov 2024 18:28:32 +0200 Subject: [PATCH 183/256] Remove comments --- tests/e2e/Adapter/Base.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 0cbd6e436..fbbd4e9b0 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6019,7 +6019,6 @@ public function testUpdateAttributeRename(): void type: Database::VAR_STRING, ); - //shmuel $collection = static::getDatabase()->getCollection('rename_test'); $this->assertEquals('renamed', $collection->getAttribute('attributes')[0]['key']); From f1c141087850b856b3327044bf73ae8a1f3644b3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 14 Nov 2024 15:30:47 +0200 Subject: [PATCH 184/256] create table bigint --- src/Database/Adapter/MariaDB.php | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 84889d15d..cf5e8494f 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -148,49 +148,49 @@ public function createCollection(string $name, array $attributes = [], array $in $indexStrings[$key] = "{$indexType} `{$indexId}` ({$indexAttributes}),"; } - $collection = " - CREATE TABLE {$this->getSQLTable($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, - PRIMARY KEY (_id), - " . \implode(' ', $attributeStrings) . " - " . \implode(' ', $indexStrings) . " - "; - if ($this->sharedTables) { - $collection .= " - _tenant INT(11) UNSIGNED DEFAULT NULL, - UNIQUE KEY _uid (_uid, _tenant), + $indexSql = ' + _tenant BIGINT UNSIGNED DEFAULT NULL, + UNIQUE KEY (_uid, _tenant), KEY _created_at (_tenant, _createdAt), KEY _updated_at (_tenant, _updatedAt), - KEY _tenant_id (_tenant, _id) - "; + KEY _tenant_id (_tenant, _id), + '; } else { - $collection .= " + $indexSql = ' UNIQUE KEY _uid (_uid), KEY _created_at (_createdAt), - KEY _updated_at (_updatedAt) - "; + KEY _updated_at (_updatedAt), + '; } - $collection .= ")"; + $collection = " + CREATE TABLE {$this->getSQLTable($id)} ( + `_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `_uid` VARCHAR(255) NOT NULL, + `_createdAt` DATETIME(3) DEFAULT NULL, + `_updatedAt` DATETIME(3) DEFAULT NULL, + `_permissions` MEDIUMTEXT DEFAULT NULL, + ".$indexSql." + ". \implode(' ', $attributeStrings) . " + ". \implode(' ', $indexStrings) . " + PRIMARY KEY (`_id`) + )"; + $collection = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collection); $permissions = " CREATE TABLE {$this->getSQLTable($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, + `_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `_type` VARCHAR(12) NOT NULL, + `_permission` VARCHAR(255) NOT NULL, + `_document` VARCHAR(255) NOT NULL, PRIMARY KEY (_id), "; if ($this->sharedTables) { $permissions .= " - _tenant INT(11) UNSIGNED DEFAULT NULL, + _tenant BIGINT UNSIGNED DEFAULT NULL, UNIQUE INDEX _index1 (_document, _tenant, _type, _permission), INDEX _permission (_tenant, _permission, _type) "; From a4498fdf695ad60b34a2f47b86808faf321b6cb6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 14 Nov 2024 15:54:28 +0200 Subject: [PATCH 185/256] Index Length share table --- src/Database/Adapter/MariaDB.php | 1 + src/Database/Adapter/SQL.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index cf5e8494f..57d62bbbd 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -215,6 +215,7 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (PDOException $e) { $e = $this->processException($e); + // Can we remove drop perms table? if (!($e instanceof DuplicateException)) { $this->getPDO() ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 7e649919d..5bef6c3e6 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1086,9 +1086,9 @@ public function getMaxVarcharLength(): int public function getMaxIndexLength(): int { /** - * $tenant int = 1 + * $tenant bigint requires 2 index length */ - return $this->sharedTables ? 767 : 768; + return $this->sharedTables ? 766 : 768; } /** From 5e54e78f02de9dbeb47d257cf2f8eea5b1b63e1b Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 14 Nov 2024 16:00:26 +0200 Subject: [PATCH 186/256] Postgres --- src/Database/Adapter/Postgres.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index d47478c12..0514dc757 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -197,11 +197,11 @@ public function createCollection(string $name, array $attributes = [], array $in $attributeStrings[] = "\"{$attrId}\" {$attrType}, "; } - $sqlTenant = $this->sharedTables ? '_tenant INTEGER DEFAULT NULL,' : ''; + $sqlTenant = $this->sharedTables ? '_tenant BIGINT DEFAULT NULL,' : ''; $collection = " CREATE TABLE {$this->getSQLTable($id)} ( - _id SERIAL NOT NULL, + _id BIGSERIAL NOT NULL, _uid VARCHAR(255) NOT NULL, ". $sqlTenant ." \"_createdAt\" TIMESTAMP(3) DEFAULT NULL, @@ -231,8 +231,8 @@ public function createCollection(string $name, array $attributes = [], array $in $permissions = " CREATE TABLE {$this->getSQLTable($id . '_perms')} ( - _id SERIAL NOT NULL, - _tenant INTEGER DEFAULT NULL, + _id BIGSERIAL NOT NULL, + _tenant BIGINT DEFAULT NULL, _type VARCHAR(12) NOT NULL, _permission VARCHAR(255) NOT NULL, _document VARCHAR(255) NOT NULL, From a8d3f8d34546d1d12c5953a61ee94e36ac1fb8d9 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 14 Nov 2024 16:18:54 +0200 Subject: [PATCH 187/256] Check index length for big int --- src/Database/Validator/Index.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 2270a6e0f..0357a5abb 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -185,10 +185,17 @@ public function checkIndexLength(Document $index): bool $attributeSize = $attribute->getAttribute('size', 0); $indexLength = $lengths[$attributePosition] ?? $attributeSize; break; + case Database::VAR_FLOAT: $attributeSize = 2; // 8 bytes / 4 mb4 $indexLength = 2; break; + + case Database::VAR_INTEGER: + $attributeSize = $attribute->getAttribute('size', 0); + $indexLength = $attributeSize = $attributeSize >= 8 ? 2 : 1; // bigint + break; + default: $attributeSize = 1; // 4 bytes / 4 mb4 $indexLength = 1; From c50eeb76256bddbb7df8f4c9731224b7020bb161 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 15 Nov 2024 14:53:02 +0900 Subject: [PATCH 188/256] Fix create documents event --- src/Database/Database.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index b16d08500..be0132815 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3376,7 +3376,10 @@ public function createDocuments(string $collection, array $documents, int $batch $documents[$key] = $this->decode($collection, $document); } - $this->trigger(self::EVENT_DOCUMENTS_CREATE, $documents); + $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ + '$collection' => $collection->getId(), + 'modified' => array_map(fn ($document) => $document->getId(), $documents) + ])); return $documents; } From be5032a063187aa69f93dd9606f2ee01d10447e1 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 15 Nov 2024 14:54:26 +0900 Subject: [PATCH 189/256] Run Linter --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index be0132815..911c45f2d 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3376,7 +3376,7 @@ public function createDocuments(string $collection, array $documents, int $batch $documents[$key] = $this->decode($collection, $document); } - $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ + $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ '$collection' => $collection->getId(), 'modified' => array_map(fn ($document) => $document->getId(), $documents) ])); From 99b2cb5cbd4898b80c6010ab7ba0d4c646cb0e5c Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 15 Nov 2024 16:13:54 +0900 Subject: [PATCH 190/256] Don't throw when deleting indexes that don't exist --- src/Database/Adapter/MariaDB.php | 14 +++++++++++--- src/Database/Adapter/SQLite.php | 14 +++++++++++--- tests/e2e/Adapter/Base.php | 18 ++++++++++++++++++ tests/e2e/Adapter/MariaDBTest.php | 10 ++++++++++ tests/e2e/Adapter/MirrorTest.php | 15 +++++++++++++++ tests/e2e/Adapter/MongoDBTest.php | 5 +++++ tests/e2e/Adapter/MySQLTest.php | 10 ++++++++++ tests/e2e/Adapter/PostgresTest.php | 11 +++++++++++ tests/e2e/Adapter/SQLiteTest.php | 10 ++++++++++ tests/e2e/Adapter/SharedTables/MariaDBTest.php | 10 ++++++++++ tests/e2e/Adapter/SharedTables/MongoDBTest.php | 5 +++++ tests/e2e/Adapter/SharedTables/MySQLTest.php | 10 ++++++++++ .../e2e/Adapter/SharedTables/PostgresTest.php | 11 +++++++++++ tests/e2e/Adapter/SharedTables/SQLiteTest.php | 10 ++++++++++ 14 files changed, 147 insertions(+), 6 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 84889d15d..50672a113 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -832,9 +832,17 @@ public function deleteIndex(string $collection, string $id): bool $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); - return $this->getPDO() - ->prepare($sql) - ->execute(); + try { + return $this->getPDO() + ->prepare($sql) + ->execute(); + } catch (PDOException $e) { + if ($e->getCode() === "42000" && $e->errorInfo[1] === 1091) { + return true; + } + + throw $e; + } } /** diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 90008bb09..005b58564 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -499,9 +499,17 @@ public function deleteIndex(string $collection, string $id): bool $sql = "DROP INDEX `{$this->getNamespace()}_{$this->tenant}_{$name}_{$id}`"; $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); - return $this->getPDO() - ->prepare($sql) - ->execute(); + try { + return $this->getPDO() + ->prepare($sql) + ->execute(); + } catch (PDOException $e) { + if (str_contains($e->getMessage(), 'no such index')) { + return true; + } + + throw $e; + } } /** diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a66d96991..6e6e8a245 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -49,6 +49,14 @@ abstract protected static function getDatabase(): Database; */ abstract protected static function deleteColumn(string $collection, string $column): bool; + /** + * @param string $collection + * @param string $index + * + * @return bool + */ + abstract protected static function deleteIndex(string $collection, string $index): bool; + /** * @return string */ @@ -1616,6 +1624,16 @@ public function testCreateDeleteIndex(): void $this->assertInstanceOf(DuplicateException::class, $e); } + // Test delete index when index does not exist + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::deleteIndex('indexes', 'index1')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index1')); + + // Test delete index when attribute does not exist + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('indexes', 'string')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index1')); + static::getDatabase()->deleteCollection('indexes'); } diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index 10a217d4f..32d8c0c0c 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -70,4 +70,14 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "DROP INDEX `{$index}` ON {$sqlTable}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/MirrorTest.php b/tests/e2e/Adapter/MirrorTest.php index ae8bd7185..a3d457624 100644 --- a/tests/e2e/Adapter/MirrorTest.php +++ b/tests/e2e/Adapter/MirrorTest.php @@ -331,4 +331,19 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $sqlTable = "`" . self::$source->getDatabase() . "`.`" . self::$source->getNamespace() . "_" . $collection . "`"; + $sql = "DROP INDEX `{$index}` ON {$sqlTable}"; + + self::$sourcePdo->exec($sql); + + $sqlTable = "`" . self::$destination->getDatabase() . "`.`" . self::$destination->getNamespace() . "_" . $collection . "`"; + $sql = "DROP INDEX `{$index}` ON {$sqlTable}"; + + self::$destinationPdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index 9bd560a8f..e582aef59 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -100,4 +100,9 @@ protected static function deleteColumn(string $collection, string $column): bool { return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + return true; + } } diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index 1cd3eb2e8..369958f07 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -72,4 +72,14 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "DROP INDEX `{$index}` ON {$sqlTable}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index ebf2cbc11..62977c913 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -69,4 +69,15 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $key = "\"".self::getDatabase()->getNamespace()."_".self::getDatabase()->getTenant()."_{$collection}_{$index}\""; + + $sql = "DROP INDEX \"".self::getDatabase()->getDatabase()."\".{$key}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index 0718d5522..87676b7f5 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -75,4 +75,14 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $index = "`".self::getDatabase()->getNamespace()."_".self::getDatabase()->getTenant()."_{$collection}_{$index}`"; + $sql = "DROP INDEX {$index}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/MariaDBTest.php b/tests/e2e/Adapter/SharedTables/MariaDBTest.php index f6d58a30f..989159e53 100644 --- a/tests/e2e/Adapter/SharedTables/MariaDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MariaDBTest.php @@ -73,4 +73,14 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "DROP INDEX `{$index}` ON {$sqlTable}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/MongoDBTest.php b/tests/e2e/Adapter/SharedTables/MongoDBTest.php index 51027b88f..7d41bb711 100644 --- a/tests/e2e/Adapter/SharedTables/MongoDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MongoDBTest.php @@ -103,4 +103,9 @@ protected static function deleteColumn(string $collection, string $column): bool { return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/MySQLTest.php b/tests/e2e/Adapter/SharedTables/MySQLTest.php index 4cd682319..e56a2ba51 100644 --- a/tests/e2e/Adapter/SharedTables/MySQLTest.php +++ b/tests/e2e/Adapter/SharedTables/MySQLTest.php @@ -75,4 +75,14 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; + $sql = "DROP INDEX `{$index}` ON {$sqlTable}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/PostgresTest.php b/tests/e2e/Adapter/SharedTables/PostgresTest.php index 23c659958..8319311d5 100644 --- a/tests/e2e/Adapter/SharedTables/PostgresTest.php +++ b/tests/e2e/Adapter/SharedTables/PostgresTest.php @@ -72,4 +72,15 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $key = "\"".self::getDatabase()->getNamespace()."_".self::getDatabase()->getTenant()."_{$collection}_{$index}\""; + + $sql = "DROP INDEX \"".self::getDatabase()->getDatabase()."\".{$key}"; + + self::$pdo->exec($sql); + + return true; + } } diff --git a/tests/e2e/Adapter/SharedTables/SQLiteTest.php b/tests/e2e/Adapter/SharedTables/SQLiteTest.php index 7cea4fb61..daa39d6be 100644 --- a/tests/e2e/Adapter/SharedTables/SQLiteTest.php +++ b/tests/e2e/Adapter/SharedTables/SQLiteTest.php @@ -78,4 +78,14 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } + + protected static function deleteIndex(string $collection, string $index): bool + { + $index = "`".self::getDatabase()->getNamespace()."_".self::getDatabase()->getTenant()."_{$collection}_{$index}`"; + $sql = "DROP INDEX {$index}"; + + self::$pdo->exec($sql); + + return true; + } } From d705a023ad186fb2a8484773983c2b41b6e66e50 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 15 Nov 2024 16:17:15 +0900 Subject: [PATCH 191/256] Run Linter --- tests/e2e/Adapter/Base.php | 2 +- tests/e2e/Adapter/SharedTables/MariaDBTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6e6e8a245..72622dd31 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -52,7 +52,7 @@ abstract protected static function deleteColumn(string $collection, string $colu /** * @param string $collection * @param string $index - * + * * @return bool */ abstract protected static function deleteIndex(string $collection, string $index): bool; diff --git a/tests/e2e/Adapter/SharedTables/MariaDBTest.php b/tests/e2e/Adapter/SharedTables/MariaDBTest.php index 989159e53..b747c3244 100644 --- a/tests/e2e/Adapter/SharedTables/MariaDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MariaDBTest.php @@ -73,7 +73,7 @@ protected static function deleteColumn(string $collection, string $column): bool return true; } - + protected static function deleteIndex(string $collection, string $index): bool { $sqlTable = "`" . self::getDatabase()->getDatabase() . "`.`" . self::getDatabase()->getNamespace() . "_" . $collection . "`"; From 0f9fc7387e0b6206189d606dbf5cbe3c193447e7 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 18 Nov 2024 21:37:12 +0200 Subject: [PATCH 192/256] Remove comment --- src/Database/Adapter/MariaDB.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 57d62bbbd..cf5e8494f 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -215,7 +215,6 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (PDOException $e) { $e = $this->processException($e); - // Can we remove drop perms table? if (!($e instanceof DuplicateException)) { $this->getPDO() ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") From 1512ff34bde57398a7bc21b25c8c9f0399bb6537 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 20 Nov 2024 16:42:37 +0900 Subject: [PATCH 193/256] Change Bulk Signatures to give out documents instead of count --- src/Database/Database.php | 55 ++++++++++++------------ src/Database/Mirror.php | 20 ++++----- tests/e2e/Adapter/Base.php | 86 ++++++++++++++++---------------------- 3 files changed, 72 insertions(+), 89 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 93a36b23a..0a91f1345 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3970,15 +3970,15 @@ public function updateDocument(string $collection, string $id, Document $documen * @param array $queries * @param int $batchSize * - * @return int + * @return array * * @throws AuthorizationException * @throws DatabaseException */ - public function updateDocuments(string $collection, Document $updates, array $queries = [], int $batchSize = self::INSERT_BATCH_SIZE): int + public function updateDocuments(string $collection, Document $updates, array $queries = [], int $batchSize = self::INSERT_BATCH_SIZE): array { if ($updates->isEmpty()) { - return 0; + return []; } $collection = $this->silent(fn () => $this->getCollection($collection)); @@ -4033,11 +4033,10 @@ public function updateDocuments(string $collection, Document $updates, array $qu throw new StructureException($validator->getDescription()); } - $affectedDocumentIds = []; + $allModifiedDocuments = []; - $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor, $affectedDocumentIds) { + $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor, $allModifiedDocuments) { $lastDocument = null; - $totalModified = 0; $documentSecurity = $collection->getAttribute('documentSecurity', false); @@ -4078,9 +4077,8 @@ public function updateDocuments(string $collection, Document $updates, array $qu $newDocument = array_merge($document->getArrayCopy(), $updates->getArrayCopy()); $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, new Document($newDocument))); + $allModifiedDocuments[] = new Document($newDocument); } - - $affectedDocumentIds[] = $document->getId(); } $getResults = fn () => $this->adapter->updateDocuments( @@ -4089,13 +4087,11 @@ public function updateDocuments(string $collection, Document $updates, array $qu $affectedDocuments ); - $result = $skipAuth ? $authorization->skip($getResults) : $getResults(); - - $totalModified += $result; + $skipAuth ? $authorization->skip($getResults) : $getResults(); if (count($affectedDocuments) < $batchSize) { break; - } elseif ($originalLimit && count($affectedDocumentIds) == $originalLimit) { + } elseif ($originalLimit && count($allModifiedDocuments) == $originalLimit) { break; } @@ -4104,15 +4100,15 @@ public function updateDocuments(string $collection, Document $updates, array $qu $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ '$collection' => $collection->getId(), - 'modified' => $affectedDocumentIds, + 'modified' => array_map(fn ($document) => $document->getId(), $allModifiedDocuments) ])); - foreach ($affectedDocumentIds as $id) { - $this->purgeRelatedDocuments($collection, $id); - $this->purgeCachedDocument($collection->getId(), $id); + foreach ($allModifiedDocuments as $document) { + $this->purgeRelatedDocuments($collection, $document->getId()); + $this->purgeCachedDocument($collection->getId(), $document->getId()); } - return $totalModified; + return $allModifiedDocuments; }); return $affected; @@ -5150,13 +5146,13 @@ private function deleteCascade(Document $collection, Document $relatedCollection * @param array $queries * @param int $batchSize * - * @return int + * @return Array * * @throws AuthorizationException * @throws DatabaseException * @throws RestrictedException */ - public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): int + public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): array { if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); @@ -5193,12 +5189,11 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba throw new DatabaseException("cursor Document must be from the same Collection."); } - $affectedDocumentIds = []; - - $deleted = $this->withTransaction(function () use ($collection, $queries, $batchSize, $affectedDocumentIds, $limit, $cursor) { + $allAffectedDocuments = $this->withTransaction(function () use ($collection, $queries, $batchSize, $limit, $cursor) { $documentSecurity = $collection->getAttribute('documentSecurity', false); $authorization = new Authorization(self::PERMISSION_DELETE); $skipAuth = $authorization->isValid($collection->getDelete()); + $allAffectedDocuments = []; if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { throw new AuthorizationException($authorization->getDescription()); @@ -5228,7 +5223,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba break; } - $affectedDocumentIds = array_merge($affectedDocumentIds, array_map(fn ($document) => $document->getId(), $affectedDocuments)); + $allAffectedDocuments = array_merge($affectedDocuments, $allAffectedDocuments); foreach ($affectedDocuments as $document) { // Delete Relationships @@ -5242,27 +5237,29 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba if (count($affectedDocuments) < $batchSize) { break; - } elseif ($originalLimit && count($affectedDocumentIds) == $originalLimit) { + } elseif ($originalLimit && count($allAffectedDocuments) == $originalLimit) { break; } $lastDocument = end($affectedDocuments); } - if (empty($affectedDocumentIds)) { - return 0; + if (empty($allAffectedDocuments)) { + return []; } $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ '$collection' => $collection->getId(), - 'modified' => $affectedDocumentIds + 'modified' => $allAffectedDocuments ])); // Mass delete using adapter with query - return $this->adapter->deleteDocuments($collection->getId(), $affectedDocumentIds); + $this->adapter->deleteDocuments($collection->getId(), array_map(fn ($document) => $document->getId(), $allAffectedDocuments)); + + return $allAffectedDocuments; }); - return $deleted; + return $allAffectedDocuments; } /** diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index a01a72022..f21b97f43 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -662,19 +662,19 @@ public function updateDocuments( Document $updates, array $queries = [], int $batchSize = self::INSERT_BATCH_SIZE - ): int { - $count = $this->source->updateDocuments($collection, $updates, $queries, $batchSize); + ): array { + $documents = $this->source->updateDocuments($collection, $updates, $queries, $batchSize); if ( \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) || $this->destination === null ) { - return $count; + return $documents; } $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { - return $count; + return $documents; } try { @@ -707,7 +707,7 @@ public function updateDocuments( $this->logError('updateDocuments', $err); } - return $count; + return $documents; } public function deleteDocument(string $collection, string $id): bool @@ -753,20 +753,20 @@ public function deleteDocument(string $collection, string $id): bool return $result; } - public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): int + public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): array { - $count = $this->source->deleteDocuments($collection, $queries, $batchSize); + $documents = $this->source->deleteDocuments($collection, $queries, $batchSize); if ( \in_array($collection, self::SOURCE_ONLY_COLLECTIONS) || $this->destination === null ) { - return $count; + return $documents; } $upgrade = $this->silent(fn () => $this->getUpgradeStatus($collection)); if ($upgrade === null || $upgrade->getAttribute('status', '') !== 'upgraded') { - return $count; + return $documents; } try { @@ -793,7 +793,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $this->logError('deleteDocuments', $err); } - return $count; + return $documents; } public function updateAttributeRequired(string $collection, string $id, bool $required): Document diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 72622dd31..2c22496e7 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15897,8 +15897,7 @@ public function testDeleteBulkDocuments(): void $this->assertCount(10, $docs); // TEST: Bulk Delete All Documents - $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); - $this->assertEquals(10, $deleted); + $this->assertCount(10, static::getDatabase()->deleteDocuments('bulk_delete')); $docs = static::getDatabase()->find('bulk_delete'); $this->assertCount(0, $docs); @@ -15906,10 +15905,9 @@ public function testDeleteBulkDocuments(): void // TEST: Bulk delete documents with queries. $this->propegateBulkDocuments('bulk_delete'); - $deleted = static::getDatabase()->deleteDocuments('bulk_delete', [ + $this->assertCount(5,static::getDatabase()->deleteDocuments('bulk_delete', [ Query::greaterThanEqual('integer', 5) - ]); - $this->assertEquals(5, $deleted); + ])); $docs = static::getDatabase()->find('bulk_delete'); $this->assertCount(5, $docs); @@ -15927,9 +15925,8 @@ public function testDeleteBulkDocuments(): void Permission::read(Role::any()), Permission::delete(Role::any()) ], false); - $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); - $this->assertEquals(5, $deleted); + $this->assertCount(5, static::getDatabase()->deleteDocuments('bulk_delete')); $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete'))); // TEST: Make sure we can't delete documents we don't have permissions for @@ -15938,8 +15935,7 @@ public function testDeleteBulkDocuments(): void ], true); $this->propegateBulkDocuments('bulk_delete', documentSecurity: true); - $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); - $this->assertEquals(0, $deleted); + $this->assertCount(0, static::getDatabase()->deleteDocuments('bulk_delete')); $documents = Authorization::skip(function () { return static::getDatabase()->find('bulk_delete'); @@ -15967,7 +15963,7 @@ public function testDeleteBulkDocumentsQueries(): void } static::getDatabase()->createCollection( - 'bulk_delete', + 'bulk_delete_queries', attributes: [ new Document([ '$id' => 'text', @@ -15991,36 +15987,36 @@ public function testDeleteBulkDocumentsQueries(): void ); // Test limit - $this->propegateBulkDocuments('bulk_delete'); + $this->propegateBulkDocuments('bulk_delete_queries'); - $this->assertEquals(5, static::getDatabase()->deleteDocuments('bulk_delete', [Query::limit(5)])); - $this->assertCount(5, static::getDatabase()->find('bulk_delete')); + $this->assertCount(5, static::getDatabase()->deleteDocuments('bulk_delete_queries', [Query::limit(5)])); + $this->assertCount(5, static::getDatabase()->find('bulk_delete_queries')); - $this->assertEquals(5, static::getDatabase()->deleteDocuments('bulk_delete', [Query::limit(5)])); - $this->assertCount(0, static::getDatabase()->find('bulk_delete')); + $this->assertCount(5, static::getDatabase()->deleteDocuments('bulk_delete_queries', [Query::limit(5)])); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_queries')); // Test Limit more than batchSize - $this->propegateBulkDocuments('bulk_delete', Database::DELETE_BATCH_SIZE * 2); - $this->assertCount(Database::DELETE_BATCH_SIZE * 2, static::getDatabase()->find('bulk_delete', [Query::limit(200)])); + $this->propegateBulkDocuments('bulk_delete_queries', Database::DELETE_BATCH_SIZE * 2); + $this->assertCount(Database::DELETE_BATCH_SIZE * 2, static::getDatabase()->find('bulk_delete_queries', [Query::limit(200)])); - $this->assertEquals(Database::DELETE_BATCH_SIZE + 2, static::getDatabase()->deleteDocuments('bulk_delete', [Query::limit(Database::DELETE_BATCH_SIZE + 2)])); + $this->assertCount(Database::DELETE_BATCH_SIZE + 2, static::getDatabase()->deleteDocuments('bulk_delete_queries', [Query::limit(Database::DELETE_BATCH_SIZE + 2)])); - $this->assertCount(Database::DELETE_BATCH_SIZE - 2, static::getDatabase()->find('bulk_delete', [Query::limit(200)])); - $this->assertEquals(Database::DELETE_BATCH_SIZE - 2, $this->getDatabase()->deleteDocuments('bulk_delete')); + $this->assertCount(Database::DELETE_BATCH_SIZE - 2, static::getDatabase()->find('bulk_delete_queries', [Query::limit(200)])); + $this->assertCount(Database::DELETE_BATCH_SIZE - 2, $this->getDatabase()->deleteDocuments('bulk_delete_queries')); // Test Offset - $this->propegateBulkDocuments('bulk_delete', 100); - $this->assertEquals(50, static::getDatabase()->deleteDocuments('bulk_delete', [Query::offset(50)])); + $this->propegateBulkDocuments('bulk_delete_queries', 100); + $this->assertCount(50, static::getDatabase()->deleteDocuments('bulk_delete_queries', [Query::offset(50)])); - $docs = static::getDatabase()->find('bulk_delete', [Query::limit(100)]); + $docs = static::getDatabase()->find('bulk_delete_queries', [Query::limit(100)]); $this->assertCount(50, $docs); $lastDoc = end($docs); $this->assertNotEmpty($lastDoc); $this->assertEquals('doc49', $lastDoc->getId()); - $this->assertEquals(50, static::getDatabase()->deleteDocuments('bulk_delete')); + $this->assertCount(50, static::getDatabase()->deleteDocuments('bulk_delete_queries')); - static::getDatabase()->deleteCollection('bulk_delete'); + static::getDatabase()->deleteCollection('bulk_delete_queries'); } public function testDeleteBulkDocumentsOneToOneRelationship(): void @@ -16604,19 +16600,17 @@ public function testUpdateDocuments(): void } // Test Update half of the documents - $affected = static::getDatabase()->updateDocuments($collection, new Document([ + $this->assertCount(5, static::getDatabase()->updateDocuments($collection, new Document([ 'string' => 'text📝 updated', ]), [ Query::greaterThanEqual('integer', 5), - ]); - - $this->assertEquals($affected, 5); + ])); $updatedDocuments = static::getDatabase()->find($collection, [ Query::greaterThanEqual('integer', 5), ]); - $this->assertEquals(count($updatedDocuments), 5); + $this->assertCount(5, $updatedDocuments); foreach ($updatedDocuments as $document) { $this->assertEquals('text📝 updated', $document->getAttribute('string')); @@ -16633,11 +16627,9 @@ public function testUpdateDocuments(): void } // Test Update all documents - $affected = static::getDatabase()->updateDocuments($collection, new Document([ + $this->assertCount(10, static::getDatabase()->updateDocuments($collection, new Document([ 'string' => 'text📝 updated all', - ])); - - $this->assertEquals(10, $affected); + ]))); $updatedDocuments = static::getDatabase()->find($collection); @@ -16711,14 +16703,12 @@ public function testUpdateDocuments(): void }); // Test we can update more documents than batchSize - $affected = static::getDatabase()->updateDocuments($collection, new Document([ + $this->assertCount(10, static::getDatabase()->updateDocuments($collection, new Document([ 'string' => 'batchSize Test' - ]), batchSize: 2); + ]), batchSize: 2)); $documents = static::getDatabase()->find($collection); - $this->assertEquals(10, $affected); - foreach ($documents as $document) { $this->assertEquals('batchSize Test', $document->getAttribute('string')); } @@ -16759,23 +16749,19 @@ public function testUpdateDocumentsQueries(): void // Test limit $this->propegateBulkDocuments($collection, 100); - $affected = static::getDatabase()->updateDocuments($collection, new Document([ + $this->assertCount(10, static::getDatabase()->updateDocuments($collection, new Document([ 'text' => 'text📝 updated', - ]), [Query::limit(10)]); - - $this->assertEquals(10, $affected); + ]), [Query::limit(10)])); $this->assertCount(10, static::getDatabase()->find($collection, [Query::equal('text', ['text📝 updated'])])); - $this->assertEquals(100, static::getDatabase()->deleteDocuments($collection)); + $this->assertCount(100, static::getDatabase()->deleteDocuments($collection)); $this->assertCount(0, static::getDatabase()->find($collection)); // Test Offset $this->propegateBulkDocuments($collection, 100); - - $affected = static::getDatabase()->updateDocuments($collection, new Document([ + $this->assertCount(50, static::getDatabase()->updateDocuments($collection, new Document([ 'text' => 'text📝 updated', - ]), [Query::offset(50)]); + ]), [Query::offset(50)])); - $this->assertEquals(50, $affected); $docs = static::getDatabase()->find($collection, [Query::equal('text', ['text📝 updated']), Query::limit(100)]); $this->assertCount(50, $docs); @@ -16783,7 +16769,7 @@ public function testUpdateDocumentsQueries(): void $this->assertNotEmpty($lastDoc); $this->assertEquals('doc99', $lastDoc->getId()); - $this->assertEquals(100, static::getDatabase()->deleteDocuments($collection)); + $this->assertCount(100, static::getDatabase()->deleteDocuments($collection)); } public function testUpdateDocumentsPermissions(): void @@ -16844,7 +16830,7 @@ public function testUpdateDocumentsPermissions(): void return static::getDatabase()->find($collection); }); - $this->assertEquals(10, $affected); + $this->assertCount(10, $affected); $this->assertCount(11, $documents); $modifiedDocuments = array_filter($documents, function (Document $document) { @@ -16882,7 +16868,7 @@ public function testUpdateDocumentsPermissions(): void 'string' => 'text📝 updated', ])); - $this->assertEquals(10, $affected); + $this->assertCount(10, $affected); $documents = Authorization::skip(function () use ($collection) { return $this->getDatabase()->find($collection); From 97b510bef0add0493c1ff2b0ed242a84093e24d8 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 20 Nov 2024 16:48:23 +0900 Subject: [PATCH 194/256] Make tests checked returned documents --- tests/e2e/Adapter/Base.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 2c22496e7..3a83e92f1 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15905,9 +15905,14 @@ public function testDeleteBulkDocuments(): void // TEST: Bulk delete documents with queries. $this->propegateBulkDocuments('bulk_delete'); - $this->assertCount(5,static::getDatabase()->deleteDocuments('bulk_delete', [ + $modified = static::getDatabase()->deleteDocuments('bulk_delete', [ Query::greaterThanEqual('integer', 5) - ])); + ]); + $this->assertCount(5,$modified); + + foreach ($modified as $document) { + $this->assertGreaterThanOrEqual(5, $document->getAttribute('integer')); + } $docs = static::getDatabase()->find('bulk_delete'); $this->assertCount(5, $docs); @@ -16600,11 +16605,16 @@ public function testUpdateDocuments(): void } // Test Update half of the documents - $this->assertCount(5, static::getDatabase()->updateDocuments($collection, new Document([ + $modified = static::getDatabase()->updateDocuments($collection, new Document([ 'string' => 'text📝 updated', ]), [ Query::greaterThanEqual('integer', 5), - ])); + ]); + $this->assertCount(5, $modified); + + foreach ($modified as $document) { + $this->assertEquals('text📝 updated', $document->getAttribute('string')); + } $updatedDocuments = static::getDatabase()->find($collection, [ Query::greaterThanEqual('integer', 5), @@ -16614,6 +16624,7 @@ public function testUpdateDocuments(): void foreach ($updatedDocuments as $document) { $this->assertEquals('text📝 updated', $document->getAttribute('string')); + $this->assertGreaterThanOrEqual(5, $document->getAttribute('integer')); } $controlDocuments = static::getDatabase()->find($collection, [ From fa67c0f1c7dc42ea2b7a913d427a74271f1bd9f0 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 21 Nov 2024 13:21:07 +0900 Subject: [PATCH 195/256] Address comments, fix tests --- src/Database/Database.php | 50 ++++++++++++++++---------------------- tests/e2e/Adapter/Base.php | 10 +++++--- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 0a91f1345..835392c73 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4033,10 +4033,9 @@ public function updateDocuments(string $collection, Document $updates, array $qu throw new StructureException($validator->getDescription()); } - $allModifiedDocuments = []; - - $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor, $allModifiedDocuments) { + $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor) { $lastDocument = null; + $documents = []; $documentSecurity = $collection->getAttribute('documentSecurity', false); @@ -4074,10 +4073,9 @@ public function updateDocuments(string $collection, Document $updates, array $qu foreach ($affectedDocuments as $document) { if ($this->resolveRelationships) { - $newDocument = array_merge($document->getArrayCopy(), $updates->getArrayCopy()); - - $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, new Document($newDocument))); - $allModifiedDocuments[] = new Document($newDocument); + $newDocument = new Document(array_merge($document->getArrayCopy(), $updates->getArrayCopy())); + $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, $newDocument)); + $documents[] = $newDocument; } } @@ -4091,24 +4089,20 @@ public function updateDocuments(string $collection, Document $updates, array $qu if (count($affectedDocuments) < $batchSize) { break; - } elseif ($originalLimit && count($allModifiedDocuments) == $originalLimit) { + } elseif ($originalLimit && count($documents) == $originalLimit) { break; } $lastDocument = end($affectedDocuments); } - $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ - '$collection' => $collection->getId(), - 'modified' => array_map(fn ($document) => $document->getId(), $allModifiedDocuments) - ])); - - foreach ($allModifiedDocuments as $document) { + foreach ($documents as $document) { $this->purgeRelatedDocuments($collection, $document->getId()); $this->purgeCachedDocument($collection->getId(), $document->getId()); + $this->trigger(self::EVENT_DOCUMENT_UPDATE, $document); } - return $allModifiedDocuments; + return $documents; }); return $affected; @@ -5146,7 +5140,7 @@ private function deleteCascade(Document $collection, Document $relatedCollection * @param array $queries * @param int $batchSize * - * @return Array + * @return array * * @throws AuthorizationException * @throws DatabaseException @@ -5189,11 +5183,11 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba throw new DatabaseException("cursor Document must be from the same Collection."); } - $allAffectedDocuments = $this->withTransaction(function () use ($collection, $queries, $batchSize, $limit, $cursor) { + $modified = $this->withTransaction(function () use ($collection, $queries, $batchSize, $limit, $cursor) { $documentSecurity = $collection->getAttribute('documentSecurity', false); $authorization = new Authorization(self::PERMISSION_DELETE); $skipAuth = $authorization->isValid($collection->getDelete()); - $allAffectedDocuments = []; + $documents = []; if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { throw new AuthorizationException($authorization->getDescription()); @@ -5223,7 +5217,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba break; } - $allAffectedDocuments = array_merge($affectedDocuments, $allAffectedDocuments); + $documents = array_merge($affectedDocuments, $documents); foreach ($affectedDocuments as $document) { // Delete Relationships @@ -5237,29 +5231,27 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba if (count($affectedDocuments) < $batchSize) { break; - } elseif ($originalLimit && count($allAffectedDocuments) == $originalLimit) { + } elseif ($originalLimit && count($documents) == $originalLimit) { break; } $lastDocument = end($affectedDocuments); } - if (empty($allAffectedDocuments)) { + if (empty($documents)) { return []; } - $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ - '$collection' => $collection->getId(), - 'modified' => $allAffectedDocuments - ])); + foreach ($documents as $document) { + $this->trigger(self::EVENT_DOCUMENT_DELETE, $document); + } - // Mass delete using adapter with query - $this->adapter->deleteDocuments($collection->getId(), array_map(fn ($document) => $document->getId(), $allAffectedDocuments)); + $this->adapter->deleteDocuments($collection->getId(), array_map(fn ($document) => $document->getId(), $documents)); - return $allAffectedDocuments; + return $documents; }); - return $allAffectedDocuments; + return $modified; } /** diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 3a83e92f1..04188b830 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -17037,10 +17037,13 @@ public function testEvents(): void Database::EVENT_DOCUMENT_INCREASE, Database::EVENT_DOCUMENT_DECREASE, Database::EVENT_DOCUMENTS_CREATE, - Database::EVENT_DOCUMENTS_UPDATE, + Database::EVENT_DOCUMENT_UPDATE, + Database::EVENT_DOCUMENT_UPDATE, + Database::EVENT_DOCUMENT_UPDATE, Database::EVENT_INDEX_DELETE, Database::EVENT_DOCUMENT_DELETE, - Database::EVENT_DOCUMENTS_DELETE, + Database::EVENT_DOCUMENT_DELETE, + Database::EVENT_DOCUMENT_DELETE, Database::EVENT_ATTRIBUTE_DELETE, Database::EVENT_COLLECTION_DELETE, Database::EVENT_DATABASE_DELETE @@ -17048,7 +17051,6 @@ public function testEvents(): void $database->on(Database::EVENT_ALL, 'test', function ($event, $data) use (&$events) { $shifted = array_shift($events); - $this->assertEquals($shifted, $event); }); @@ -17115,6 +17117,8 @@ public function testEvents(): void $database->deleteIndex($collectionId, $indexId1); $database->deleteDocument($collectionId, 'doc1'); + + var_dump('asd'); $database->deleteDocuments($collectionId); $database->deleteAttribute($collectionId, 'attr1'); $database->deleteCollection($collectionId); From 67b871be6197153fa35470130a93cdaab7b2e8a3 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 21 Nov 2024 13:22:06 +0900 Subject: [PATCH 196/256] Run Formatter --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 04188b830..fabaa564f 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15908,7 +15908,7 @@ public function testDeleteBulkDocuments(): void $modified = static::getDatabase()->deleteDocuments('bulk_delete', [ Query::greaterThanEqual('integer', 5) ]); - $this->assertCount(5,$modified); + $this->assertCount(5, $modified); foreach ($modified as $document) { $this->assertGreaterThanOrEqual(5, $document->getAttribute('integer')); From 4590937f1849879b388e1dc11de1bbea72635ffc Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Thu, 21 Nov 2024 13:33:21 +0900 Subject: [PATCH 197/256] Address comments --- src/Database/Database.php | 8 ++++---- tests/e2e/Adapter/Base.php | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 835392c73..d4e2dc151 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4033,7 +4033,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu throw new StructureException($validator->getDescription()); } - $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor) { + $documents = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor) { $lastDocument = null; $documents = []; @@ -4105,7 +4105,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu return $documents; }); - return $affected; + return $documents; } /** @@ -5183,7 +5183,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba throw new DatabaseException("cursor Document must be from the same Collection."); } - $modified = $this->withTransaction(function () use ($collection, $queries, $batchSize, $limit, $cursor) { + $documents = $this->withTransaction(function () use ($collection, $queries, $batchSize, $limit, $cursor) { $documentSecurity = $collection->getAttribute('documentSecurity', false); $authorization = new Authorization(self::PERMISSION_DELETE); $skipAuth = $authorization->isValid($collection->getDelete()); @@ -5251,7 +5251,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba return $documents; }); - return $modified; + return $documents; } /** diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index fabaa564f..ea18ea2a7 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -17118,7 +17118,6 @@ public function testEvents(): void $database->deleteIndex($collectionId, $indexId1); $database->deleteDocument($collectionId, 'doc1'); - var_dump('asd'); $database->deleteDocuments($collectionId); $database->deleteAttribute($collectionId, 'attr1'); $database->deleteCollection($collectionId); From 5431c4616f8d1cb1b732d309ec5597c4d6403736 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 22 Nov 2024 12:07:03 +0900 Subject: [PATCH 198/256] Update Create Documents to fire individual events --- src/Database/Database.php | 6 +----- tests/e2e/Adapter/Base.php | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index d4e2dc151..3f086360a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3425,13 +3425,9 @@ public function createDocuments(string $collection, array $documents, int $batch } $documents[$key] = $this->decode($collection, $document); + $this->trigger(self::EVENT_DOCUMENT_CREATE, $documents[$key]); } - $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ - '$collection' => $collection->getId(), - 'modified' => array_map(fn ($document) => $document->getId(), $documents) - ])); - return $documents; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index ea18ea2a7..5a141718e 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -17036,7 +17036,8 @@ public function testEvents(): void Database::EVENT_DOCUMENT_SUM, Database::EVENT_DOCUMENT_INCREASE, Database::EVENT_DOCUMENT_DECREASE, - Database::EVENT_DOCUMENTS_CREATE, + Database::EVENT_DOCUMENT_CREATE, + Database::EVENT_DOCUMENT_CREATE, Database::EVENT_DOCUMENT_UPDATE, Database::EVENT_DOCUMENT_UPDATE, Database::EVENT_DOCUMENT_UPDATE, From 69e210d4a214d2a463a0fa1df491c9f3d888c4c0 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 22 Nov 2024 12:24:33 +0900 Subject: [PATCH 199/256] Address PHP 8.4 Deprecations for PHPStan --- composer.lock | 56 +++++++++++++++---------------- src/Database/Adapter.php | 2 +- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Adapter/Mongo.php | 4 +-- src/Database/Adapter/Postgres.php | 4 +-- src/Database/Adapter/SQLite.php | 4 +-- src/Database/Database.php | 10 +++--- src/Database/Exception.php | 2 +- src/Database/Mirror.php | 8 ++--- 9 files changed, 46 insertions(+), 46 deletions(-) diff --git a/composer.lock b/composer.lock index 3151d0c17..d08a958c7 100644 --- a/composer.lock +++ b/composer.lock @@ -193,28 +193,28 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.0.6", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", "shasum": "" }, "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", "extra": { @@ -246,9 +246,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" }, - "time": "2024-03-08T09:58:59+00:00" + "time": "2024-11-18T16:19:46+00:00" }, { "name": "mongodb/mongodb", @@ -2097,16 +2097,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.12", + "version": "0.33.14", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "bfb7812df9e489b3cba7d5504a49ce578c71af1f" + "reference": "45a5a2db3602fa054096f378482c7da9936f5850" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/bfb7812df9e489b3cba7d5504a49ce578c71af1f", - "reference": "bfb7812df9e489b3cba7d5504a49ce578c71af1f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/45a5a2db3602fa054096f378482c7da9936f5850", + "reference": "45a5a2db3602fa054096f378482c7da9936f5850", "shasum": "" }, "require": { @@ -2138,9 +2138,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.12" + "source": "https://github.com/utopia-php/http/tree/0.33.14" }, - "time": "2024-11-13T12:45:45+00:00" + "time": "2024-11-20T12:39:10+00:00" }, { "name": "utopia-php/mongo", @@ -2389,16 +2389,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.1", + "version": "v1.18.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" + "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", - "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", + "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", "shasum": "" }, "require": { @@ -2451,7 +2451,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-24T17:22:50+00:00" + "time": "2024-11-20T09:33:46+00:00" }, { "name": "myclabs/deep-copy", @@ -2723,16 +2723,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.10", + "version": "1.12.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" + "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", - "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "shasum": "" }, "require": { @@ -2777,7 +2777,7 @@ "type": "github" } ], - "time": "2024-11-11T15:37:09+00:00" + "time": "2024-11-17T14:08:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4348,7 +4348,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4356,6 +4356,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index ae6627f97..f5748e460 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -481,7 +481,7 @@ abstract public function createAttribute(string $collection, string $id, string * * @return bool */ - abstract public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, string $newKey = null): bool; + abstract public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null): bool; /** * Delete Attribute diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 50672a113..e0e514fcc 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -394,7 +394,7 @@ public function createAttribute(string $collection, string $id, string $type, in * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, string $newKey = null): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null): bool { $name = $this->filter($collection); $id = $this->filter($id); diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index c259ead09..4a54c473b 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -122,7 +122,7 @@ public function create(string $name): bool * @return bool * @throws Exception */ - public function exists(string $database, string $collection = null): bool + public function exists(string $database, ?string $collection = null): bool { if (!\is_null($collection)) { $collection = $this->getNamespace() . "_" . $collection; @@ -1007,7 +1007,7 @@ public function deleteDocuments(string $collection, array $ids): int * * @return bool */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, string $newKey = null): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null): bool { if (!empty($newKey) && $newKey !== $id) { return $this->renameAttribute($collection, $id, $newKey); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index d47478c12..c679a9414 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -478,12 +478,12 @@ public function renameAttribute(string $collection, string $old, string $new): b * @param int $size * @param bool $signed * @param bool $array - * @param string $newKey + * @param string|null $newKey * @return bool * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, string $newKey = null): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null): bool { $name = $this->filter($collection); $id = $this->filter($id); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 005b58564..a52ce1332 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -332,12 +332,12 @@ public function analyzeCollection(string $collection): bool * @param int $size * @param bool $signed * @param bool $array - * @param string $newKey + * @param string|null $newKey * @return bool * @throws Exception * @throws PDOException */ - public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, string $newKey = null): bool + public function updateAttribute(string $collection, string $id, string $type, int $size, bool $signed = true, bool $array = false, ?string $newKey = null): bool { if (!empty($newKey) && $newKey !== $id) { return $this->renameAttribute($collection, $id, $newKey); diff --git a/src/Database/Database.php b/src/Database/Database.php index 3f086360a..976806271 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -477,7 +477,7 @@ public function before(string $event, string $name, callable $callback): static * @param array|null $listeners List of listeners to silence; if null, all listeners will be silenced * @return T */ - public function silent(callable $callback, array $listeners = null): mixed + public function silent(callable $callback, ?array $listeners = null): mixed { $previous = $this->silentListeners; @@ -1143,14 +1143,14 @@ public function delete(?string $database = null): bool * @param string $id * @param array $attributes * @param array $indexes - * @param array $permissions + * @param array|null $permissions * @param bool $documentSecurity * @return Document * @throws DatabaseException * @throws DuplicateException * @throws LimitException */ - public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document + public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document { $permissions ??= [ Permission::create(Role::any()), @@ -1437,7 +1437,7 @@ public function deleteCollection(string $id): bool * @throws StructureException * @throws Exception */ - 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 + 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 { $collection = $this->silent(fn () => $this->getCollection($collection)); @@ -1802,7 +1802,7 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de * @return Document * @throws Exception */ - public function updateAttribute(string $collection, string $id, string $type = null, int $size = null, bool $required = null, mixed $default = null, bool $signed = null, bool $array = null, string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { return $this->updateAttributeMeta($collection, $id, function ($attribute, $collectionDoc, $attributeIndex) use ($collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters, $newKey) { $altering = !\is_null($type) diff --git a/src/Database/Exception.php b/src/Database/Exception.php index 94099c6ae..d86e94c2b 100644 --- a/src/Database/Exception.php +++ b/src/Database/Exception.php @@ -6,7 +6,7 @@ class Exception extends \Exception { - public function __construct(string $message, int|string $code = 0, Throwable $previous = null) + public function __construct(string $message, int|string $code = 0, ?Throwable $previous = null) { if (\is_string($code)) { if (\is_numeric($code)) { diff --git a/src/Database/Mirror.php b/src/Database/Mirror.php index f21b97f43..92cdae23c 100644 --- a/src/Database/Mirror.php +++ b/src/Database/Mirror.php @@ -169,7 +169,7 @@ protected function trigger(string $event, mixed $args = null): void $this->source->trigger($event, $args); } - public function silent(callable $callback, array $listeners = null): mixed + public function silent(callable $callback, ?array $listeners = null): mixed { return $this->source->silent($callback, $listeners); } @@ -194,7 +194,7 @@ public function delete(?string $database = null): bool return $this->delegate(__FUNCTION__, \func_get_args()); } - public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document + public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document { $result = $this->source->createCollection( $id, @@ -292,7 +292,7 @@ public function deleteCollection(string $id): bool return $result; } - public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, string $format = null, array $formatOptions = [], array $filters = []): bool + public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, ?string $format = null, array $formatOptions = [], array $filters = []): bool { $result = $this->source->createAttribute( $collection, @@ -356,7 +356,7 @@ public function createAttribute(string $collection, string $id, string $type, in return $result; } - public function updateAttribute(string $collection, string $id, string $type = null, int $size = null, bool $required = null, mixed $default = null, bool $signed = null, bool $array = null, string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { $document = $this->source->updateAttribute( $collection, From f29c159caf6582b35147317ba17c492064160b2b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 25 Nov 2024 21:06:49 +1300 Subject: [PATCH 200/256] Adapter create after limit checks --- src/Database/Database.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 976806271..23d31eaf2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1191,8 +1191,6 @@ public function createCollection(string $id, array $attributes = [], array $inde } } - $this->adapter->createCollection($id, $attributes, $indexes); - if ($id === self::METADATA) { return new Document(self::COLLECTION); } @@ -1208,17 +1206,19 @@ public function createCollection(string $id, array $attributes = [], array $inde $this->adapter->getLimitForAttributes() > 0 && $this->adapter->getCountOfAttributes($collection) > $this->adapter->getLimitForAttributes() ) { - throw new LimitException('Column limit of ' . $this->adapter->getLimitForAttributes() . ' exceeded. Cannot create collection.'); + throw new LimitException('Attribute limit of ' . $this->adapter->getLimitForAttributes() . ' exceeded. Cannot create collection.'); } if ( $this->adapter->getDocumentSizeLimit() > 0 && $this->adapter->getAttributeWidth($collection) > $this->adapter->getDocumentSizeLimit() ) { - throw new LimitException('Row width limit of ' . $this->adapter->getDocumentSizeLimit() . ' exceeded. Cannot create collection.'); + throw new LimitException('Document size limit of ' . $this->adapter->getDocumentSizeLimit() . ' exceeded. Cannot create collection.'); } } + $this->adapter->createCollection($id, $attributes, $indexes); + $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); $this->trigger(self::EVENT_COLLECTION_CREATE, $createdCollection); @@ -1410,6 +1410,8 @@ public function deleteCollection(string $id): bool $this->trigger(self::EVENT_COLLECTION_DELETE, $collection); } + $this->purgeCachedCollection($id); + return $deleted; } @@ -1521,7 +1523,7 @@ public function createAttribute(string $collection, string $id, string $type, in // Only execute when $default is given if (!\is_null($default)) { if ($required === true) { - throw new DatabaseException('Cannot set a default value on a required attribute'); + throw new DatabaseException('Cannot set a default value for a required attribute'); } $this->validateDefaultTypes($type, $default); From 0e5801755059e3953804b191bf448e1e2cd01594 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 25 Nov 2024 21:08:45 +1300 Subject: [PATCH 201/256] Fix meta check --- src/Database/Database.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 23d31eaf2..d8f4860c0 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1191,10 +1191,6 @@ public function createCollection(string $id, array $attributes = [], array $inde } } - if ($id === self::METADATA) { - return new Document(self::COLLECTION); - } - // Check index limits, if given if ($indexes && $this->adapter->getCountOfIndexes($collection) > $this->adapter->getLimitForIndexes()) { throw new LimitException('Index limit of ' . $this->adapter->getLimitForIndexes() . ' exceeded. Cannot create collection.'); @@ -1219,6 +1215,10 @@ public function createCollection(string $id, array $attributes = [], array $inde $this->adapter->createCollection($id, $attributes, $indexes); + if ($id === self::METADATA) { + return new Document(self::COLLECTION); + } + $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); $this->trigger(self::EVENT_COLLECTION_CREATE, $createdCollection); From 43180e3fbe997af4dbdad4e6f5341acafb3bb3bd Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 25 Nov 2024 21:38:49 +1300 Subject: [PATCH 202/256] Delete table if metadata create failed --- src/Database/Database.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index d8f4860c0..7764554b4 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1219,7 +1219,12 @@ public function createCollection(string $id, array $attributes = [], array $inde return new Document(self::COLLECTION); } - $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); + try { + $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); + } catch (Exception $e) { + $this->adapter->deleteCollection($id); + throw $e; + } $this->trigger(self::EVENT_COLLECTION_CREATE, $createdCollection); From bd502439b3ea9dc29847b0edd917e031599f0af1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 26 Nov 2024 15:55:07 +1300 Subject: [PATCH 203/256] Dump on metadata create fail --- src/Database/Database.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 7764554b4..50987c197 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1222,6 +1222,7 @@ public function createCollection(string $id, array $attributes = [], array $inde try { $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); } catch (Exception $e) { + \var_dump('Error creating collection ' . $id . ' metadata document. Deleting collection.' . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); $this->adapter->deleteCollection($id); throw $e; } From 38354fb891dc509006c0a81651ffc6d17796466f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 26 Nov 2024 17:10:48 +1300 Subject: [PATCH 204/256] Remove dump --- src/Database/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 50987c197..7764554b4 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1222,7 +1222,6 @@ public function createCollection(string $id, array $attributes = [], array $inde try { $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); } catch (Exception $e) { - \var_dump('Error creating collection ' . $id . ' metadata document. Deleting collection.' . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); $this->adapter->deleteCollection($id); throw $e; } From c3b8f81bee0c5514763cbd6130cb5abaded41e11 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 26 Nov 2024 18:39:27 +1300 Subject: [PATCH 205/256] Catch not found exception when checking database exists --- src/Database/Adapter/MariaDB.php | 5 ++++- src/Database/Adapter/SQL.php | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e0e514fcc..3f73497bf 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2420,7 +2420,10 @@ protected function processException(PDOException $e): \Exception return new TruncateException('Resize would result in data truncation', $e->getCode(), $e); } - + // Unknown database + if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1049) { + return new NotFoundException($e->getMessage(), $e->getCode(), $e); + } return $e; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 7e649919d..70031b6b6 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -9,6 +9,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Transaction as TransactionException; use Utopia\Database\Query; @@ -163,10 +164,19 @@ public function exists(string $database, ?string $collection = null): bool $stmt->bindValue(':schema', $database, PDO::PARAM_STR); } - $stmt->execute(); + try { + $stmt->execute(); + $document = $stmt->fetchAll(); + $stmt->closeCursor(); + } catch (PDOException $e) { + $e = $this->processException($e); - $document = $stmt->fetchAll(); - $stmt->closeCursor(); + if ($e instanceof NotFoundException) { + return false; + } + + throw $e; + } if (empty($document)) { return false; From d53d6ab3ad79b243c2979cf6cee6841de392fbda Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 26 Nov 2024 19:55:36 +1300 Subject: [PATCH 206/256] Fix stan --- src/Database/Adapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index f5748e460..76adf996e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1022,4 +1022,6 @@ abstract public function getConnectionId(): string; * @return array */ abstract public function getInternalIndexesKeys(): array; + + abstract protected function processException(\PDOException $e): \Exception; } From 6fdc4cc8a4db2f2a224daa3c999660e9409858d7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 27 Nov 2024 16:59:04 +1300 Subject: [PATCH 207/256] Debug dump --- src/Database/Database.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 976806271..9d5d43742 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3305,6 +3305,8 @@ public function createDocument(string $collection, Document $document): Document $collection = $this->silent(fn () => $this->getCollection($collection)); + \var_dump($collection); + if ($collection->getId() !== self::METADATA) { $authorization = new Authorization(self::PERMISSION_CREATE); if (!$authorization->isValid($collection->getCreate())) { From c3f03747a1bbc99fd5d5f414803ae314c037e739 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 27 Nov 2024 20:24:35 +1300 Subject: [PATCH 208/256] Remove logging --- src/Database/Database.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 9d5d43742..976806271 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3305,8 +3305,6 @@ public function createDocument(string $collection, Document $document): Document $collection = $this->silent(fn () => $this->getCollection($collection)); - \var_dump($collection); - if ($collection->getId() !== self::METADATA) { $authorization = new Authorization(self::PERMISSION_CREATE); if (!$authorization->isValid($collection->getCreate())) { From 6f991377b7e5ec443bc25a1a3c6b2912efcb6901 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 1 Dec 2024 09:45:28 +0200 Subject: [PATCH 209/256] DROP TABLE IF EXISTS --- src/Database/Adapter/MariaDB.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e0e514fcc..508835f8d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -215,11 +215,11 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (PDOException $e) { $e = $this->processException($e); - if (!($e instanceof DuplicateException)) { - $this->getPDO() - ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") - ->execute(); - } +// if (!($e instanceof DuplicateException)) { +// $this->getPDO() +// ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") +// ->execute(); +// } throw $e; } From 8a89f3462b73d236138e94b9e431ae5646a99a78 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 1 Dec 2024 09:46:44 +0200 Subject: [PATCH 210/256] DROP TABLE IF EXISTS --- src/Database/Adapter/MariaDB.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 508835f8d..e0e514fcc 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -215,11 +215,11 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (PDOException $e) { $e = $this->processException($e); -// if (!($e instanceof DuplicateException)) { -// $this->getPDO() -// ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") -// ->execute(); -// } + if (!($e instanceof DuplicateException)) { + $this->getPDO() + ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + ->execute(); + } throw $e; } From 4ddd1bd82021a8909cfdfc06a2b938b6ae5148b2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 1 Dec 2024 09:47:41 +0200 Subject: [PATCH 211/256] Remove DROP TABLE IF EXISTS --- src/Database/Adapter/MariaDB.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e0e514fcc..508835f8d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -215,11 +215,11 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (PDOException $e) { $e = $this->processException($e); - if (!($e instanceof DuplicateException)) { - $this->getPDO() - ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") - ->execute(); - } +// if (!($e instanceof DuplicateException)) { +// $this->getPDO() +// ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") +// ->execute(); +// } throw $e; } From 8efa45c6c1d169e31db9b87605350be94ef492ad Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 1 Dec 2024 09:51:12 +0200 Subject: [PATCH 212/256] lint --- src/Database/Adapter/MariaDB.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 508835f8d..d2f26ead1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -215,11 +215,11 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (PDOException $e) { $e = $this->processException($e); -// if (!($e instanceof DuplicateException)) { -// $this->getPDO() -// ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") -// ->execute(); -// } + // if (!($e instanceof DuplicateException)) { + // $this->getPDO() + // ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") + // ->execute(); + // } throw $e; } From b9940b0b38bea8baef89b60399358fac3dc44023 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 2 Dec 2024 14:31:19 +1300 Subject: [PATCH 213/256] Remove comment --- src/Database/Adapter/MariaDB.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index d2f26ead1..90ea0ae8c 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -213,15 +213,7 @@ public function createCollection(string $name, array $attributes = [], array $in ->prepare($permissions) ->execute(); } catch (PDOException $e) { - $e = $this->processException($e); - - // if (!($e instanceof DuplicateException)) { - // $this->getPDO() - // ->prepare("DROP TABLE IF EXISTS {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')};") - // ->execute(); - // } - - throw $e; + throw $this->processException($e); } return true; From e7bfd895c9ebcf44cacf89c6d3d70278b12a4f92 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 2 Dec 2024 17:58:21 +1300 Subject: [PATCH 214/256] Remove PDO specific exception from interface --- src/Database/Adapter.php | 2 -- src/Database/Adapter/MariaDB.php | 28 ++++++++++++++-------------- src/Database/Adapter/SQL.php | 5 +++++ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 76adf996e..f5748e460 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1022,6 +1022,4 @@ abstract public function getConnectionId(): string; * @return array */ abstract public function getInternalIndexesKeys(): array; - - abstract protected function processException(\PDOException $e): \Exception; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 5139015aa..b616247ab 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2379,6 +2379,20 @@ public function setTimeout(int $milliseconds, string $event = Database::EVENT_AL }); } + /** + * @return string + */ + public function getConnectionId(): string + { + $stmt = $this->getPDO()->query("SELECT CONNECTION_ID();"); + return $stmt->fetchColumn(); + } + + public function getInternalIndexesKeys(): array + { + return ['primary', '_created_at', '_updated_at', '_tenant_id']; + } + protected function processException(PDOException $e): \Exception { // Timeout @@ -2419,18 +2433,4 @@ protected function processException(PDOException $e): \Exception return $e; } - - /** - * @return string - */ - public function getConnectionId(): string - { - $stmt = $this->getPDO()->query("SELECT CONNECTION_ID();"); - return $stmt->fetchColumn(); - } - - public function getInternalIndexesKeys(): array - { - return ['primary', '_created_at', '_updated_at', '_tenant_id']; - } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 70031b6b6..a0b6a3c6a 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1146,4 +1146,9 @@ public function getInternalIndexesKeys(): array { return []; } + + protected function processException(PDOException $e): \Exception + { + return $e; + } } From 66011b149758aef0ef61a84921707f30298c8fc0 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 2 Dec 2024 16:20:12 +0900 Subject: [PATCH 215/256] Move all events into individual ones --- src/Database/Database.php | 19 ++++++++++++++----- tests/e2e/Adapter/Base.php | 10 +++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 7764554b4..925555553 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3432,9 +3432,13 @@ public function createDocuments(string $collection, array $documents, int $batch } $documents[$key] = $this->decode($collection, $document); - $this->trigger(self::EVENT_DOCUMENT_CREATE, $documents[$key]); } + $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ + '$collection' => $collection->getId(), + 'modified' => array_map(fn ($document) => $document->getId(), $documents) + ])); + return $documents; } @@ -4102,9 +4106,13 @@ public function updateDocuments(string $collection, Document $updates, array $qu foreach ($documents as $document) { $this->purgeRelatedDocuments($collection, $document->getId()); $this->purgeCachedDocument($collection->getId(), $document->getId()); - $this->trigger(self::EVENT_DOCUMENT_UPDATE, $document); } + $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ + '$collection' => $collection->getId(), + 'modified' => array_map(fn ($document) => $document->getId(), $documents) + ])); + return $documents; }); @@ -5245,9 +5253,10 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba return []; } - foreach ($documents as $document) { - $this->trigger(self::EVENT_DOCUMENT_DELETE, $document); - } + $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ + '$collection' => $collection->getId(), + 'modified' => array_map(fn ($document) => $document->getId(), $documents) + ])); $this->adapter->deleteDocuments($collection->getId(), array_map(fn ($document) => $document->getId(), $documents)); diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 5a141718e..943eb458e 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -17036,15 +17036,11 @@ public function testEvents(): void Database::EVENT_DOCUMENT_SUM, Database::EVENT_DOCUMENT_INCREASE, Database::EVENT_DOCUMENT_DECREASE, - Database::EVENT_DOCUMENT_CREATE, - Database::EVENT_DOCUMENT_CREATE, - Database::EVENT_DOCUMENT_UPDATE, - Database::EVENT_DOCUMENT_UPDATE, - Database::EVENT_DOCUMENT_UPDATE, + Database::EVENT_DOCUMENTS_CREATE, + Database::EVENT_DOCUMENTS_UPDATE, Database::EVENT_INDEX_DELETE, Database::EVENT_DOCUMENT_DELETE, - Database::EVENT_DOCUMENT_DELETE, - Database::EVENT_DOCUMENT_DELETE, + Database::EVENT_DOCUMENTS_DELETE, Database::EVENT_ATTRIBUTE_DELETE, Database::EVENT_COLLECTION_DELETE, Database::EVENT_DATABASE_DELETE From 89dbbf8bb84497c13631b04bc6f01ce33a20b633 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 2 Dec 2024 17:40:48 +0900 Subject: [PATCH 216/256] Switch to counting documents for event --- src/Database/Database.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 925555553..95a499df2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3436,7 +3436,7 @@ public function createDocuments(string $collection, array $documents, int $batch $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ '$collection' => $collection->getId(), - 'modified' => array_map(fn ($document) => $document->getId(), $documents) + 'modified' => count($documents) ])); return $documents; @@ -4110,7 +4110,7 @@ public function updateDocuments(string $collection, Document $updates, array $qu $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ '$collection' => $collection->getId(), - 'modified' => array_map(fn ($document) => $document->getId(), $documents) + 'modified' => count($documents) ])); return $documents; @@ -5255,7 +5255,7 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ '$collection' => $collection->getId(), - 'modified' => array_map(fn ($document) => $document->getId(), $documents) + 'modified' => count($documents) ])); $this->adapter->deleteDocuments($collection->getId(), array_map(fn ($document) => $document->getId(), $documents)); From 7eb005deb21bc0789275abe28d80c96919f8ae92 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Dec 2024 12:48:42 +0200 Subject: [PATCH 217/256] Schema attributes --- src/Database/Adapter.php | 7 ++++ src/Database/Adapter/MariaDB.php | 40 +++++++++++++++++++++ src/Database/Adapter/Mongo.php | 10 ++++++ src/Database/Adapter/Postgres.php | 10 ++++++ src/Database/Adapter/SQLite.php | 10 ++++++ src/Database/Database.php | 12 +++++++ tests/e2e/Adapter/Base.php | 58 +++++++++++++++++++++++++++++++ 7 files changed, 147 insertions(+) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index f5748e460..b7e214720 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -781,6 +781,13 @@ abstract public function getSupportForSchemas(): bool; */ abstract public function getSupportForAttributes(): bool; + /** + * Are schema attributes? + * + * @return bool + */ + abstract public function getSupportForSchemaAttributes(): bool; + /** * Is index supported? * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index b616247ab..701944645 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2433,4 +2433,44 @@ protected function processException(PDOException $e): \Exception return $e; } + + /** + * Get Schema Attributes + * + * @param string $collection + * @return array + * @throws DatabaseException + */ + public function getSchemaAttributes(string $collection): array + { + $schema = $this->getDatabase(); + $collection = $this->getNamespace().'_'.$this->filter($collection); + + $stmt = $this->getPDO()->prepare(" + SELECT + COLUMN_NAME, + COLUMN_DEFAULT, + IS_NULLABLE, + DATA_TYPE, + CHARACTER_MAXIMUM_LENGTH, + NUMERIC_PRECISION, + NUMERIC_SCALE, + DATETIME_PRECISION, + COLUMN_TYPE, + COLUMN_KEY, + EXTRA + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table + "); + $stmt->bindParam(':schema', $schema); + $stmt->bindParam(':table', $collection); + $stmt->execute(); + + return $stmt->fetchAll(); + } + + public function getSupportForSchemaAttributes(): bool + { + return true; + } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 4a54c473b..0fc0d123b 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1787,6 +1787,16 @@ public function getSupportForGetConnectionId(): bool return false; } + /** + * Is get schema attributes supported? + * + * @return bool + */ + public function getSupportForSchemaAttributes(): bool + { + return false; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index c679a9414..378d77cdf 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2397,6 +2397,16 @@ public function getSupportForJSONOverlaps(): bool return false; } + /** + * Is get schema attributes supported? + * + * @return bool + */ + public function getSupportForSchemaAttributes(): bool + { + return false; + } + /** * @return string */ diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index a52ce1332..8442367ff 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -934,6 +934,16 @@ public function getSupportForGetConnectionId(): bool return false; } + /** + * Is get schema attributes supported? + * + * @return bool + */ + public function getSupportForSchemaAttributes(): bool + { + return false; + } + /** * Get SQL Index Type * diff --git a/src/Database/Database.php b/src/Database/Database.php index 95a499df2..ce9b47faf 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6037,4 +6037,16 @@ public function analyzeCollection(string $collection): bool { return $this->adapter->analyzeCollection($collection); } + + /** + * Get Schema attributes + * + * @param string $collection + * @return array + */ + public function getSchemaAttributes(string $collection): array + { + var_dump('getSchemaAttributes' . $collection); + return $this->adapter->getSchemaAttributes($collection); + } } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 943eb458e..9ca27abbd 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1300,6 +1300,64 @@ public function testPurgeCollectionCache(): void $this->assertArrayHasKey('age', $document); } + public function testSchemaAttribute(): void + { + if (!$this->getDatabase()->getAdapter()->getSupportForSchemaAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collection = 'schema'; + $db = static::getDatabase(); + + $db->createCollection($collection); + + $db->createAttribute($collection, 'username', Database::VAR_STRING, 128, true); + $db->createAttribute($collection, 'story', Database::VAR_STRING, 20000, true); + $db->createAttribute($collection, 'string_list', Database::VAR_STRING, 128, true, null, true, true); + $db->createAttribute($collection, 'dob', Database::VAR_DATETIME, 0, false, '2000-06-12T14:12:55.000+00:00', true, false, null, [], ['datetime']); + + $attributes = []; + foreach ($db->getSchemaAttributes($collection) as $attribute) { + $attributes[$attribute['COLUMN_NAME']] = $attribute; + } + + $attribute = $attributes['username']; + $this->assertEquals('username', $attribute['COLUMN_NAME']); + $this->assertEquals('varchar', $attribute['DATA_TYPE']); + $this->assertEquals('varchar(128)', $attribute['COLUMN_TYPE']); + $this->assertEquals('128', $attribute['CHARACTER_MAXIMUM_LENGTH']); + $this->assertEquals('YES', $attribute['IS_NULLABLE']); + + $attribute = $attributes['story']; + $this->assertEquals('story', $attribute['COLUMN_NAME']); + $this->assertEquals('text', $attribute['DATA_TYPE']); + $this->assertEquals('text', $attribute['COLUMN_TYPE']); + $this->assertEquals('65535', $attribute['CHARACTER_MAXIMUM_LENGTH']); + + $attribute = $attributes['string_list']; + $this->assertEquals('string_list', $attribute['COLUMN_NAME']); + $this->assertTrue(in_array($attribute['DATA_TYPE'], ['json', 'longtext'])); // mysql vs maria + $this->assertTrue(in_array($attribute['COLUMN_TYPE'], ['json', 'longtext'])); + $this->assertTrue(in_array($attribute['CHARACTER_MAXIMUM_LENGTH'], [null, '4294967295'])); + $this->assertEquals('YES', $attribute['IS_NULLABLE']); + + $attribute = $attributes['dob']; + $this->assertEquals('dob', $attribute['COLUMN_NAME']); + $this->assertEquals('datetime', $attribute['DATA_TYPE']); + $this->assertEquals('datetime(3)', $attribute['COLUMN_TYPE']); + $this->assertEquals(null, $attribute['CHARACTER_MAXIMUM_LENGTH']); + $this->assertEquals('3', $attribute['DATETIME_PRECISION']); + + if($db->getSharedTables()){ + $attribute = $attributes['_tenant']; + $this->assertEquals('_tenant', $attribute['COLUMN_NAME']); + $this->assertEquals('int', $attribute['DATA_TYPE']); + $this->assertEquals('10', $attribute['NUMERIC_PRECISION']); + $this->assertTrue(in_array($attribute['COLUMN_TYPE'], ['int unsigned', 'int(11) unsigned'])); + } + } + public function testCreateDeleteAttribute(): void { static::getDatabase()->createCollection('attributes'); From aa4a0cfc9a3a26717481f22905cc9a38bc221f45 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Dec 2024 12:51:54 +0200 Subject: [PATCH 218/256] comment --- src/Database/Adapter.php | 2 +- src/Database/Database.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index b7e214720..1a4aaa240 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -782,7 +782,7 @@ abstract public function getSupportForSchemas(): bool; abstract public function getSupportForAttributes(): bool; /** - * Are schema attributes? + * Are schema attributes supported? * * @return bool */ diff --git a/src/Database/Database.php b/src/Database/Database.php index ce9b47faf..b51eff377 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6046,7 +6046,6 @@ public function analyzeCollection(string $collection): bool */ public function getSchemaAttributes(string $collection): array { - var_dump('getSchemaAttributes' . $collection); return $this->adapter->getSchemaAttributes($collection); } } From 3f7fb816e45c35e18da271763bc6ceac4824be33 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Dec 2024 12:56:38 +0200 Subject: [PATCH 219/256] lint --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 9ca27abbd..b56ac28ae 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1349,7 +1349,7 @@ public function testSchemaAttribute(): void $this->assertEquals(null, $attribute['CHARACTER_MAXIMUM_LENGTH']); $this->assertEquals('3', $attribute['DATETIME_PRECISION']); - if($db->getSharedTables()){ + if ($db->getSharedTables()) { $attribute = $attributes['_tenant']; $this->assertEquals('_tenant', $attribute['COLUMN_NAME']); $this->assertEquals('int', $attribute['DATA_TYPE']); From 67dda79e5c67251d5b2fe978024dcf87f172b122 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Dec 2024 15:26:09 +0200 Subject: [PATCH 220/256] add adapter --- src/Database/Adapter.php | 9 ++++++ src/Database/Adapter/MariaDB.php | 48 +++++++++++++++++++------------- src/Database/Adapter/Mongo.php | 5 ++++ src/Database/Adapter/SQL.php | 5 ++++ tests/e2e/Adapter/Base.php | 4 ++- 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 1a4aaa240..43b1ca130 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1029,4 +1029,13 @@ abstract public function getConnectionId(): string; * @return array */ abstract public function getInternalIndexesKeys(): array; + + /** + * Get Schema Attributes + * + * @param string $collection + * @return array + * @throws DatabaseException + */ + abstract public function getSchemaAttributes(string $collection): array; } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 701944645..10da2e971 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1457,6 +1457,7 @@ public function updateDocuments(string $collection, Document $updates, array $do $permissionsStmt->execute(); $permissions = $permissionsStmt->fetchAll(); + $permissionsStmt->closeCursor(); $initial = []; foreach (Database::PERMISSIONS as $type) { @@ -2446,27 +2447,34 @@ public function getSchemaAttributes(string $collection): array $schema = $this->getDatabase(); $collection = $this->getNamespace().'_'.$this->filter($collection); - $stmt = $this->getPDO()->prepare(" - SELECT - COLUMN_NAME, - COLUMN_DEFAULT, - IS_NULLABLE, - DATA_TYPE, - CHARACTER_MAXIMUM_LENGTH, - NUMERIC_PRECISION, - NUMERIC_SCALE, - DATETIME_PRECISION, - COLUMN_TYPE, - COLUMN_KEY, - EXTRA - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table - "); - $stmt->bindParam(':schema', $schema); - $stmt->bindParam(':table', $collection); - $stmt->execute(); + try { + $stmt = $this->getPDO()->prepare(" + SELECT + COLUMN_NAME, + COLUMN_DEFAULT, + IS_NULLABLE, + DATA_TYPE, + CHARACTER_MAXIMUM_LENGTH, + NUMERIC_PRECISION, + NUMERIC_SCALE, + DATETIME_PRECISION, + COLUMN_TYPE, + COLUMN_KEY, + EXTRA + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table + "); + $stmt->bindParam(':schema', $schema); + $stmt->bindParam(':table', $collection); + $stmt->execute(); + $results = $stmt->fetchAll(); + $stmt->closeCursor(); - return $stmt->fetchAll(); + return $results; + + } catch (PDOException $e) { + throw new DatabaseException('Failed to get schema attributes', $e->getCode(), $e); + } } public function getSupportForSchemaAttributes(): bool diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 0fc0d123b..2e35a74e0 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1952,4 +1952,9 @@ public function getInternalIndexesKeys(): array { return []; } + + public function getSchemaAttributes(string $collection): array + { + return []; + } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index a0b6a3c6a..ee69d5919 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1151,4 +1151,9 @@ protected function processException(PDOException $e): \Exception { return $e; } + + public function getSchemaAttributes(string $collection): array + { + return []; + } } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b56ac28ae..b1ed05374 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1307,9 +1307,11 @@ public function testSchemaAttribute(): void return; } - $collection = 'schema'; + $collection = 'schema_attributes'; $db = static::getDatabase(); + $this->assertEmpty($db->getSchemaAttributes('no_such_collection')); + $db->createCollection($collection); $db->createAttribute($collection, 'username', Database::VAR_STRING, 128, true); From d07b0ad52be6df50047f1ba927ce3cab1fbcf808 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 5 Dec 2024 16:01:40 +0200 Subject: [PATCH 221/256] fix phpstan --- src/Database/Adapter.php | 2 +- src/Database/Adapter/MariaDB.php | 2 +- src/Database/Database.php | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 43b1ca130..d3614fc9e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1034,7 +1034,7 @@ abstract public function getInternalIndexesKeys(): array; * Get Schema Attributes * * @param string $collection - * @return array + * @return array * @throws DatabaseException */ abstract public function getSchemaAttributes(string $collection): array; diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 10da2e971..25bfbdcce 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2439,7 +2439,7 @@ protected function processException(PDOException $e): \Exception * Get Schema Attributes * * @param string $collection - * @return array + * @return array * @throws DatabaseException */ public function getSchemaAttributes(string $collection): array diff --git a/src/Database/Database.php b/src/Database/Database.php index b51eff377..130558427 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6039,10 +6039,11 @@ public function analyzeCollection(string $collection): bool } /** - * Get Schema attributes + * Get Schema Attributes * * @param string $collection - * @return array + * @return array + * @throws DatabaseException */ public function getSchemaAttributes(string $collection): array { From 4d6fb4c5d90bf25dd6b1736928ddc52466959b6d Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 6 Dec 2024 13:08:11 +0900 Subject: [PATCH 222/256] Respect timestamps in bulk operations and add tests --- src/Database/Database.php | 17 +++++++++++++++++ tests/e2e/Adapter/Base.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Database/Database.php b/src/Database/Database.php index 95a499df2..38c0e8942 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4084,6 +4084,12 @@ public function updateDocuments(string $collection, Document $updates, array $qu $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, $newDocument)); $documents[] = $newDocument; } + + // Check if document was updated after the request timestamp + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } } $getResults = fn () => $this->adapter->updateDocuments( @@ -5236,6 +5242,17 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); } + // Check if document was updated after the request timestamp + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + if (!\is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } + $this->purgeRelatedDocuments($collection, $document->getId()); $this->purgeCachedDocument($collection->getId(), $document->getId()); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 943eb458e..9272cd002 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2110,6 +2110,8 @@ public function testCreateDocuments(): array $this->assertEquals(9223372036854775807, $document->getAttribute('bigint')); } + // Test (FAIL) create documents with invalid timestamps + return $documents; } @@ -15917,6 +15919,18 @@ public function testDeleteBulkDocuments(): void $docs = static::getDatabase()->find('bulk_delete'); $this->assertCount(5, $docs); + // TEST (FAIL): Can't delete documents in the past + $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); + + try { + $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($document) { + return $this->getDatabase()->deleteDocuments($document->getCollection()); + }); + $this->fail('Failed to throw exception'); + } catch (ConflictException $e) { + $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); + } + // TEST (FAIL): Bulk delete all documents with invalid collection permission static::getDatabase()->updateCollection('bulk_delete', [], false); try { @@ -16650,6 +16664,20 @@ public function testUpdateDocuments(): void $this->assertEquals('text📝 updated all', $document->getAttribute('string')); } + // TEST: Can't delete documents in the past + $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); + + try { + $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($collection) { + return static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'text📝 updated all', + ])); + }); + $this->fail('Failed to throw exception'); + } catch (ConflictException $e) { + $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); + } + // Check collection level permissions static::getDatabase()->updateCollection($collection, permissions: [ Permission::read(Role::user('asd')), From 6d880506ef516baa295eae170b611b4cccb9e87c Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Fri, 6 Dec 2024 13:12:39 +0900 Subject: [PATCH 223/256] Fix PHPStan --- tests/e2e/Adapter/Base.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 9272cd002..2ac1fdf14 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15923,8 +15923,8 @@ public function testDeleteBulkDocuments(): void $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); try { - $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($document) { - return $this->getDatabase()->deleteDocuments($document->getCollection()); + $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () { + return $this->getDatabase()->deleteDocuments('bulk_delete'); }); $this->fail('Failed to throw exception'); } catch (ConflictException $e) { From eea30ac9872dd7af656f18198d7fa54c9b1d602b Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 8 Dec 2024 11:17:39 +0200 Subject: [PATCH 224/256] Address comments --- src/Database/Adapter.php | 2 +- src/Database/Adapter/MariaDB.php | 36 ++++++++++++---------- tests/e2e/Adapter/Base.php | 53 +++++++++++++++++--------------- 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index d3614fc9e..6e7cc9f9e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -1034,7 +1034,7 @@ abstract public function getInternalIndexesKeys(): array; * Get Schema Attributes * * @param string $collection - * @return array + * @return array * @throws DatabaseException */ abstract public function getSchemaAttributes(string $collection): array; diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 25bfbdcce..b5d2956f1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -2439,7 +2439,7 @@ protected function processException(PDOException $e): \Exception * Get Schema Attributes * * @param string $collection - * @return array + * @return array * @throws DatabaseException */ public function getSchemaAttributes(string $collection): array @@ -2448,28 +2448,32 @@ public function getSchemaAttributes(string $collection): array $collection = $this->getNamespace().'_'.$this->filter($collection); try { - $stmt = $this->getPDO()->prepare(" - SELECT - COLUMN_NAME, - COLUMN_DEFAULT, - IS_NULLABLE, - DATA_TYPE, - CHARACTER_MAXIMUM_LENGTH, - NUMERIC_PRECISION, - NUMERIC_SCALE, - DATETIME_PRECISION, - COLUMN_TYPE, - COLUMN_KEY, - EXTRA - FROM INFORMATION_SCHEMA.COLUMNS + $stmt = $this->getPDO()->prepare(' + SELECT + COLUMN_NAME as columnName, + COLUMN_DEFAULT as columnDefault, + IS_NULLABLE as isNullable, + DATA_TYPE as dataType, + CHARACTER_MAXIMUM_LENGTH as characterMaximumLength, + NUMERIC_PRECISION as numericPrecision, + NUMERIC_SCALE as numericScale, + DATETIME_PRECISION as datetimePrecision, + COLUMN_TYPE as columnType, + COLUMN_KEY as columnKey, + EXTRA as extra + FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table - "); + '); $stmt->bindParam(':schema', $schema); $stmt->bindParam(':table', $collection); $stmt->execute(); $results = $stmt->fetchAll(); $stmt->closeCursor(); + foreach ($results as $index => $document) { + $results[$index] = new Document($document); + } + return $results; } catch (PDOException $e) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b1ed05374..6cbbfde8e 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1321,42 +1321,45 @@ public function testSchemaAttribute(): void $attributes = []; foreach ($db->getSchemaAttributes($collection) as $attribute) { - $attributes[$attribute['COLUMN_NAME']] = $attribute; - } + /** + * @var $attribute Document + */ + $attributes[$attribute->getAttribute('columnName')] = $attribute; + } $attribute = $attributes['username']; - $this->assertEquals('username', $attribute['COLUMN_NAME']); - $this->assertEquals('varchar', $attribute['DATA_TYPE']); - $this->assertEquals('varchar(128)', $attribute['COLUMN_TYPE']); - $this->assertEquals('128', $attribute['CHARACTER_MAXIMUM_LENGTH']); - $this->assertEquals('YES', $attribute['IS_NULLABLE']); + $this->assertEquals('username', $attribute['columnName']); + $this->assertEquals('varchar', $attribute['dataType']); + $this->assertEquals('varchar(128)', $attribute['columnType']); + $this->assertEquals('128', $attribute['characterMaximumLength']); + $this->assertEquals('YES', $attribute['isNullable']); $attribute = $attributes['story']; - $this->assertEquals('story', $attribute['COLUMN_NAME']); - $this->assertEquals('text', $attribute['DATA_TYPE']); - $this->assertEquals('text', $attribute['COLUMN_TYPE']); - $this->assertEquals('65535', $attribute['CHARACTER_MAXIMUM_LENGTH']); + $this->assertEquals('story', $attribute['columnName']); + $this->assertEquals('text', $attribute['dataType']); + $this->assertEquals('text', $attribute['columnType']); + $this->assertEquals('65535', $attribute['characterMaximumLength']); $attribute = $attributes['string_list']; - $this->assertEquals('string_list', $attribute['COLUMN_NAME']); - $this->assertTrue(in_array($attribute['DATA_TYPE'], ['json', 'longtext'])); // mysql vs maria - $this->assertTrue(in_array($attribute['COLUMN_TYPE'], ['json', 'longtext'])); - $this->assertTrue(in_array($attribute['CHARACTER_MAXIMUM_LENGTH'], [null, '4294967295'])); - $this->assertEquals('YES', $attribute['IS_NULLABLE']); + $this->assertEquals('string_list', $attribute['columnName']); + $this->assertTrue(in_array($attribute['dataType'], ['json', 'longtext'])); // mysql vs maria + $this->assertTrue(in_array($attribute['columnType'], ['json', 'longtext'])); + $this->assertTrue(in_array($attribute['characterMaximumLength'], [null, '4294967295'])); + $this->assertEquals('YES', $attribute['isNullable']); $attribute = $attributes['dob']; - $this->assertEquals('dob', $attribute['COLUMN_NAME']); - $this->assertEquals('datetime', $attribute['DATA_TYPE']); - $this->assertEquals('datetime(3)', $attribute['COLUMN_TYPE']); - $this->assertEquals(null, $attribute['CHARACTER_MAXIMUM_LENGTH']); - $this->assertEquals('3', $attribute['DATETIME_PRECISION']); + $this->assertEquals('dob', $attribute['columnName']); + $this->assertEquals('datetime', $attribute['dataType']); + $this->assertEquals('datetime(3)', $attribute['columnType']); + $this->assertEquals(null, $attribute['characterMaximumLength']); + $this->assertEquals('3', $attribute['datetimePrecision']); if ($db->getSharedTables()) { $attribute = $attributes['_tenant']; - $this->assertEquals('_tenant', $attribute['COLUMN_NAME']); - $this->assertEquals('int', $attribute['DATA_TYPE']); - $this->assertEquals('10', $attribute['NUMERIC_PRECISION']); - $this->assertTrue(in_array($attribute['COLUMN_TYPE'], ['int unsigned', 'int(11) unsigned'])); + $this->assertEquals('_tenant', $attribute['columnName']); + $this->assertEquals('int', $attribute['dataType']); + $this->assertEquals('10', $attribute['numericPrecision']); + $this->assertTrue(in_array($attribute['columnType'], ['int unsigned', 'int(11) unsigned'])); } } From 8470c301fddca4b0b3f7b5e8ea241bc7a5a996f8 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 8 Dec 2024 11:20:11 +0200 Subject: [PATCH 225/256] lint --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 6cbbfde8e..f64dfbaa4 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1325,7 +1325,7 @@ public function testSchemaAttribute(): void * @var $attribute Document */ $attributes[$attribute->getAttribute('columnName')] = $attribute; - } + } $attribute = $attributes['username']; $this->assertEquals('username', $attribute['columnName']); From c8086fd4a7f51228005da6e2f3ab93050d002494 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 8 Dec 2024 11:26:44 +0200 Subject: [PATCH 226/256] analyze --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index f64dfbaa4..d56a987df 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1322,7 +1322,7 @@ public function testSchemaAttribute(): void $attributes = []; foreach ($db->getSchemaAttributes($collection) as $attribute) { /** - * @var $attribute Document + * @var Document $attribute */ $attributes[$attribute->getAttribute('columnName')] = $attribute; } From 3fb18dac66bdb7fbc9cd3f7909a8aba5b8b17a01 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 8 Dec 2024 16:22:47 +0200 Subject: [PATCH 227/256] Return array of Documents --- src/Database/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 130558427..b37f821bc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6042,7 +6042,7 @@ public function analyzeCollection(string $collection): bool * Get Schema Attributes * * @param string $collection - * @return array + * @return array * @throws DatabaseException */ public function getSchemaAttributes(string $collection): array From 08e2b395114f9b97303a60c89e5beb7674075536 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 9 Dec 2024 12:21:39 +0900 Subject: [PATCH 228/256] Address Comments --- composer.json | 6 ++++++ src/Database/Database.php | 7 ++++++- tests/e2e/Adapter/Base.php | 2 -- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 66b0fad6d..71c11264f 100755 --- a/composer.json +++ b/composer.json @@ -55,5 +55,11 @@ "ext-redis": "Needed to support Redis Cache Adapter", "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", "mongodb/mongodb": "Needed to support MongoDB Database Adapter" + }, + "config": { + "allow-plugins": { + "php-http/discovery": false, + "tbachert/spi": false + } } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 38c0e8942..551e318e2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4086,7 +4086,12 @@ public function updateDocuments(string $collection, Document $updates, array $qu } // Check if document was updated after the request timestamp - $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { throw new ConflictException('Document was updated after the request timestamp'); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 2ac1fdf14..602241299 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2110,8 +2110,6 @@ public function testCreateDocuments(): array $this->assertEquals(9223372036854775807, $document->getAttribute('bigint')); } - // Test (FAIL) create documents with invalid timestamps - return $documents; } From aa8f7cb348bca739af3b06895adedd551ce85889 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 10:30:37 +0200 Subject: [PATCH 229/256] Dependency Exception --- composer.lock | 173 +++++++++++++------------- src/Database/Database.php | 53 +++++--- src/Database/Exception/Dependency.php | 7 ++ tests/e2e/Adapter/Base.php | 21 ++++ 4 files changed, 149 insertions(+), 105 deletions(-) create mode 100644 src/Database/Exception/Dependency.php diff --git a/composer.lock b/composer.lock index d08a958c7..a73ee5ada 100644 --- a/composer.lock +++ b/composer.lock @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.28.3", + "version": "v4.29.2", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100" + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c5c311e0f3d89928251ac5a2f0e3db283612c100", - "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/79aa5014efeeec3d137df5cdb0ae2fc163953945", + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.28.3" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.2" }, - "time": "2024-10-22T22:27:17+00:00" + "time": "2024-12-18T14:11:12+00:00" }, { "name": "jean85/pretty-package-versions", @@ -465,16 +465,16 @@ }, { "name": "open-telemetry/api", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6" + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/542064815d38a6df55af7957cd6f1d7d967c99c6", - "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", "shasum": "" }, "require": { @@ -488,13 +488,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.1.x-dev" - }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] + }, + "branch-alias": { + "dev-main": "1.1.x-dev" } }, "autoload": { @@ -531,7 +531,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-10-15T22:42:37+00:00" + "time": "2024-11-16T04:32:30+00:00" }, { "name": "open-telemetry/context", @@ -758,13 +758,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.0.x-dev" - }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] + }, + "branch-alias": { + "dev-main": "1.0.x-dev" } }, "autoload": { @@ -1391,16 +1391,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -1438,7 +1438,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -1454,30 +1454,31 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/http-client", - "version": "v7.1.8", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a" + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", - "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ff4df2b68d1c67abb9fef146e6540ea16b58d99e", + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { + "amphp/amp": "<2.5", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -1488,14 +1489,14 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", @@ -1532,7 +1533,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.1.8" + "source": "https://github.com/symfony/http-client/tree/v7.2.1" }, "funding": [ { @@ -1548,20 +1549,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:40:27+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -1569,12 +1570,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1610,7 +1611,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -1626,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1654,8 +1655,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1728,8 +1729,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1808,8 +1809,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1866,16 +1867,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -1929,7 +1930,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -1945,7 +1946,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "tbachert/spi", @@ -1974,10 +1975,10 @@ }, "type": "composer-plugin", "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", "branch-alias": { "dev-main": "0.2.x-dev" }, - "class": "Nevay\\SPI\\Composer\\Plugin", "plugin-optional": true }, "autoload": { @@ -2097,16 +2098,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.14", + "version": "0.33.15", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "45a5a2db3602fa054096f378482c7da9936f5850" + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/45a5a2db3602fa054096f378482c7da9936f5850", - "reference": "45a5a2db3602fa054096f378482c7da9936f5850", + "url": "https://api.github.com/repos/utopia-php/http/zipball/83b0628900c2c53e8c3efbf069f3e13050295edc", + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc", "shasum": "" }, "require": { @@ -2138,9 +2139,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.14" + "source": "https://github.com/utopia-php/http/tree/0.33.15" }, - "time": "2024-11-20T12:39:10+00:00" + "time": "2024-12-10T13:07:04+00:00" }, { "name": "utopia-php/mongo", @@ -2389,16 +2390,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.2", + "version": "v1.18.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64" + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", - "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", + "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", "shasum": "" }, "require": { @@ -2409,13 +2410,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.64.0", - "illuminate/view": "^10.48.20", - "larastan/larastan": "^2.9.8", + "friendsofphp/php-cs-fixer": "^3.65.0", + "illuminate/view": "^10.48.24", + "larastan/larastan": "^2.9.11", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.1" + "nunomaduro/termwind": "^1.17.0", + "pestphp/pest": "^2.36.0" }, "bin": [ "builds/pint" @@ -2451,7 +2452,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-20T09:33:46+00:00" + "time": "2024-11-26T15:34:00+00:00" }, { "name": "myclabs/deep-copy", @@ -2723,16 +2724,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.11", + "version": "1.12.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" + "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b469068840cfa031e1deaf2fa1886d00e20680f", + "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f", "shasum": "" }, "require": { @@ -2777,7 +2778,7 @@ "type": "github" } ], - "time": "2024-11-17T14:08:01+00:00" + "time": "2024-12-17T17:00:20+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3100,16 +3101,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -3120,7 +3121,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -3183,7 +3184,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -3199,7 +3200,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -4348,7 +4349,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4356,6 +4357,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/Database/Database.php b/src/Database/Database.php index 730c37581..7c7c9d0d1 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -15,6 +15,7 @@ use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; +use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -2063,38 +2064,54 @@ public function deleteAttribute(string $collection, string $id): bool public function renameAttribute(string $collection, string $old, string $new): bool { $collection = $this->silent(fn () => $this->getCollection($collection)); + + /** + * @var array $attributes + */ $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); - $attribute = \in_array($old, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); + /** + * @var array $indexes + */ + $indexes = $collection->getAttribute('indexes', []); - if ($attribute === false) { - throw new NotFoundException('Attribute not found'); - } + $attribute = new Document(); - $attributeNew = \in_array($new, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); + foreach ($attributes as $value) { + if($value->getId() === $old){ + $attribute = $value; + } - if ($attributeNew !== false) { - throw new DuplicateException('Attribute name already used'); + if($value->getId() === $new){ + throw new DuplicateException('Attribute name already used'); + } } - foreach ($attributes as $key => $value) { - if (isset($value['$id']) && $value['$id'] === $old) { - $attributes[$key]['key'] = $new; - $attributes[$key]['$id'] = $new; - $attributeNew = $attributes[$key]; - break; - } + if ($attribute->isEmpty()) { + throw new NotFoundException('Attribute not found'); } + $attribute->setAttribute('$id', $new); + $attribute->setAttribute('key', $new); + foreach ($indexes as $index) { $indexAttributes = $index->getAttribute('attributes', []); - $indexAttributes = \array_map(fn ($attribute) => ($attribute === $old) ? $new : $attribute, $indexAttributes); + foreach ($indexAttributes as $key => $indexAttribute) { + if($indexAttribute === $old){ + if($attribute->getAttribute('array', false)){ + throw new DependencyException("Can't rename attribute because of functional index dependency must drop index first."); + } + + $indexAttributes[$key] = $new; + } + } $index->setAttribute('attributes', $indexAttributes); } + $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new); + $collection->setAttribute('attributes', $attributes); $collection->setAttribute('indexes', $indexes); @@ -2102,9 +2119,7 @@ public function renameAttribute(string $collection, string $old, string $new): b $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new); - - $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attributeNew); + $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute); return $renamed; } diff --git a/src/Database/Exception/Dependency.php b/src/Database/Exception/Dependency.php new file mode 100644 index 000000000..c090f4748 --- /dev/null +++ b/src/Database/Exception/Dependency.php @@ -0,0 +1,7 @@ +assertEquals(true, $database->createAttribute( $collection, 'names', @@ -2766,6 +2767,15 @@ public function testArrayAttribute(): void array: true )); + $this->assertEquals(true, $database->createAttribute( + $collection, + 'cards', + Database::VAR_STRING, + size: 5000, + required: false, + array: true + )); + $this->assertEquals(true, $database->createAttribute( $collection, 'numbers', @@ -2889,6 +2899,17 @@ public function testArrayAttribute(): void $this->assertEquals('Antony', $document->getAttribute('names')[1]); $this->assertEquals(100, $document->getAttribute('numbers')[1]); + /** + * functional index dependency cannot be dropped or rename + */ + $database->createIndex($collection, 'idx_cards', Database::INDEX_KEY, ['cards']); + try { + $database->renameAttribute($collection, 'cards', 'cards_new'); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertEquals("Can't rename attribute because of functional index dependency must drop index first.", $e->getMessage()); + } + try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); From 8282694b5a55def06a52c513006aa77ab2ab2afd Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 10:52:20 +0200 Subject: [PATCH 230/256] deleteAttribute functional index dependency --- tests/e2e/Adapter/Base.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 8b38ee43d..5699102d0 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2903,6 +2903,9 @@ public function testArrayAttribute(): void * functional index dependency cannot be dropped or rename */ $database->createIndex($collection, 'idx_cards', Database::INDEX_KEY, ['cards']); + + $database->deleteAttribute($collection, 'cards'); + try { $database->renameAttribute($collection, 'cards', 'cards_new'); $this->fail('Failed to throw exception'); From 4a9cd839907068a28f884c501c3add9faa35e410 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 12:41:44 +0200 Subject: [PATCH 231/256] getSupportForCastIndexArray --- src/Database/Adapter.php | 7 +++++++ src/Database/Adapter/MariaDB.php | 24 +++++++++++------------- src/Database/Adapter/Mongo.php | 5 +++++ src/Database/Adapter/MySQL.php | 11 +++++++---- src/Database/Adapter/SQL.php | 5 +++++ src/Database/Database.php | 6 +++--- tests/e2e/Adapter/Base.php | 26 ++++++++++++++++++++------ 7 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 6e7cc9f9e..4ad975706 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -868,6 +868,13 @@ abstract public function getSupportForAttributeResizing(): bool; */ abstract public function getSupportForGetConnectionId(): bool; + /** + * Is cast index as array supported? + * + * @return bool + */ + abstract public function getSupportForCastIndexArray(): bool; + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index b5d2956f1..104d2aab8 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -433,12 +433,13 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa return $this->getPDO() ->prepare($sql) ->execute(); + } catch (PDOException $e) { if ($e->getCode() === "42000" && $e->errorInfo[1] === 1091) { return true; } - throw $e; + throw $this->processException($e); } } @@ -462,9 +463,14 @@ public function renameAttribute(string $collection, string $old, string $new): b $sql = $this->trigger(Database::EVENT_ATTRIBUTE_UPDATE, $sql); - return $this->getPDO() - ->prepare($sql) - ->execute(); + try { + return $this->getPDO() + ->prepare($sql) + ->execute(); + + } catch (PDOException $e) { + throw $this->processException($e); + } } /** @@ -766,7 +772,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; - if (!empty($collectionAttribute['array']) && $this->castIndexArray()) { + if (!empty($collectionAttribute['array']) && $this->getSupportForCastIndexArray()) { $attributes[$i] = '(CAST(' . $attr . ' AS char(' . Database::ARRAY_INDEX_LENGTH . ') ARRAY))'; } } @@ -798,14 +804,6 @@ public function createIndex(string $collection, string $id, string $type, array } } - /** - * @return bool - */ - public function castIndexArray(): bool - { - return false; - } - /** * Delete Index * diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 2e35a74e0..bae6ca9db 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1797,6 +1797,11 @@ public function getSupportForSchemaAttributes(): bool return false; } + public function getSupportForCastIndexArray(): bool + { + return false; + } + /** * Get current attribute count from collection document * diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 7564d5a51..fb1694cd0 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -5,6 +5,7 @@ use PDOException; use Utopia\Database\Database; use Utopia\Database\Exception as DatabaseException; +use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Exception\Timeout as TimeoutException; class MySQL extends MariaDB @@ -74,10 +75,7 @@ public function getSizeOfCollectionOnDisk(string $collection): int return $size; } - /** - * @return bool - */ - public function castIndexArray(): bool + public function getSupportForCastIndexArray(): bool { return true; } @@ -89,6 +87,11 @@ protected function processException(PDOException $e): \Exception return new TimeoutException($e->getMessage(), $e->getCode(), $e); } + // Functional index dependency + if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3837) { + return new DependencyException($e->errorInfo[2], $e->getCode(), $e); + } + return parent::processException($e); } } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index ee69d5919..725a91a50 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -846,6 +846,11 @@ public function getSupportForQueryContains(): bool */ abstract public function getSupportForJSONOverlaps(): bool; + public function getSupportForCastIndexArray(): bool + { + return false; + } + public function getSupportForRelationships(): bool { return true; diff --git a/src/Database/Database.php b/src/Database/Database.php index 7c7c9d0d1..8ec8ebe9e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2099,9 +2099,9 @@ public function renameAttribute(string $collection, string $old, string $new): b foreach ($indexAttributes as $key => $indexAttribute) { if($indexAttribute === $old){ - if($attribute->getAttribute('array', false)){ - throw new DependencyException("Can't rename attribute because of functional index dependency must drop index first."); - } +// if($attribute->getAttribute('array', false)){ +// throw new DependencyException("Can't rename attribute because of functional index dependency must drop index first."); +// } $indexAttributes[$key] = $new; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 5699102d0..b1c1a66c5 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -20,6 +20,7 @@ use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; +use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -2904,13 +2905,26 @@ public function testArrayAttribute(): void */ $database->createIndex($collection, 'idx_cards', Database::INDEX_KEY, ['cards']); - $database->deleteAttribute($collection, 'cards'); + if ($this->getDatabase()->getAdapter()->getSupportForCastIndexArray()) { + try { + $database->deleteAttribute($collection, 'cards'); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(DependencyException::class, $e); + $this->assertEquals("Column 'cards' has a functional index dependency and cannot be dropped or renamed.", $e->getMessage()); + } - try { - $database->renameAttribute($collection, 'cards', 'cards_new'); - $this->fail('Failed to throw exception'); - } catch (Throwable $e) { - $this->assertEquals("Can't rename attribute because of functional index dependency must drop index first.", $e->getMessage()); + try { + $database->renameAttribute($collection, 'cards', 'cards_new'); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(DependencyException::class, $e); + $this->assertEquals("Column 'cards' has a functional index dependency and cannot be dropped or renamed.", $e->getMessage()); + } + } + else { + $this->assertTrue($database->renameAttribute($collection, 'cards', 'cards_new')); + $this->assertTrue($database->deleteAttribute($collection, 'cards')); } try { From 93355a9039a73c19be45355e480682b1805d6bba Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 13:03:42 +0200 Subject: [PATCH 232/256] formatting --- src/Database/Database.php | 14 +++++++------- src/Database/Exception/Dependency.php | 4 +++- tests/e2e/Adapter/Base.php | 7 +++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 8ec8ebe9e..6d089c4fc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -7,6 +7,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Conflict as ConflictException; +use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; @@ -15,7 +16,6 @@ use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; -use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -2078,11 +2078,11 @@ public function renameAttribute(string $collection, string $old, string $new): b $attribute = new Document(); foreach ($attributes as $value) { - if($value->getId() === $old){ + if ($value->getId() === $old) { $attribute = $value; } - if($value->getId() === $new){ + if ($value->getId() === $new) { throw new DuplicateException('Attribute name already used'); } } @@ -2098,10 +2098,10 @@ public function renameAttribute(string $collection, string $old, string $new): b $indexAttributes = $index->getAttribute('attributes', []); foreach ($indexAttributes as $key => $indexAttribute) { - if($indexAttribute === $old){ -// if($attribute->getAttribute('array', false)){ -// throw new DependencyException("Can't rename attribute because of functional index dependency must drop index first."); -// } + if ($indexAttribute === $old) { + // if($attribute->getAttribute('array', false)){ + // throw new DependencyException("Can't rename attribute because of functional index dependency must drop index first."); + // } $indexAttributes[$key] = $new; } diff --git a/src/Database/Exception/Dependency.php b/src/Database/Exception/Dependency.php index c090f4748..5c58ef63c 100644 --- a/src/Database/Exception/Dependency.php +++ b/src/Database/Exception/Dependency.php @@ -4,4 +4,6 @@ use Utopia\Database\Exception; -class Dependency extends Exception {} +class Dependency extends Exception +{ +} diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b1c1a66c5..38ec0c41c 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -12,6 +12,7 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Conflict as ConflictException; +use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\Query as QueryException; @@ -20,7 +21,6 @@ use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; -use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -2921,10 +2921,9 @@ public function testArrayAttribute(): void $this->assertInstanceOf(DependencyException::class, $e); $this->assertEquals("Column 'cards' has a functional index dependency and cannot be dropped or renamed.", $e->getMessage()); } - } - else { + } else { $this->assertTrue($database->renameAttribute($collection, 'cards', 'cards_new')); - $this->assertTrue($database->deleteAttribute($collection, 'cards')); + $this->assertTrue($database->deleteAttribute($collection, 'cards_new')); } try { From 894f1ec966a5a6ef528f40cd70fc376c562eb0c6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 14:57:38 +0200 Subject: [PATCH 233/256] Add length --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 38ec0c41c..d16f9630d 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2903,7 +2903,7 @@ public function testArrayAttribute(): void /** * functional index dependency cannot be dropped or rename */ - $database->createIndex($collection, 'idx_cards', Database::INDEX_KEY, ['cards']); + $database->createIndex($collection, 'idx_cards', Database::INDEX_KEY, ['cards'], [100]); if ($this->getDatabase()->getAdapter()->getSupportForCastIndexArray()) { try { From db7d41d544c64a301364624ae53394a483feb3fa Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 15:05:28 +0200 Subject: [PATCH 234/256] Remove try catch --- src/Database/Database.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 730c37581..a63347482 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1219,12 +1219,7 @@ public function createCollection(string $id, array $attributes = [], array $inde return new Document(self::COLLECTION); } - try { - $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); - } catch (Exception $e) { - $this->adapter->deleteCollection($id); - throw $e; - } + $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); $this->trigger(self::EVENT_COLLECTION_CREATE, $createdCollection); From ca2cb861af0abe1400d5f276b16b0e815787f0cd Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 15:06:39 +0200 Subject: [PATCH 235/256] Adapter first --- src/Database/Database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index a63347482..ec37547ca 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2090,6 +2090,8 @@ public function renameAttribute(string $collection, string $old, string $new): b $index->setAttribute('attributes', $indexAttributes); } + $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new); + $collection->setAttribute('attributes', $attributes); $collection->setAttribute('indexes', $indexes); @@ -2097,8 +2099,6 @@ public function renameAttribute(string $collection, string $old, string $new): b $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new); - $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attributeNew); return $renamed; From dd5f2641106f51d469b2c118f442c15bd4ce937c Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 16:19:02 +0200 Subject: [PATCH 236/256] lock --- composer.lock | 173 +++++++++++++++++++++++++------------------------- 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/composer.lock b/composer.lock index d08a958c7..a73ee5ada 100644 --- a/composer.lock +++ b/composer.lock @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.28.3", + "version": "v4.29.2", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100" + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c5c311e0f3d89928251ac5a2f0e3db283612c100", - "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/79aa5014efeeec3d137df5cdb0ae2fc163953945", + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.28.3" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.2" }, - "time": "2024-10-22T22:27:17+00:00" + "time": "2024-12-18T14:11:12+00:00" }, { "name": "jean85/pretty-package-versions", @@ -465,16 +465,16 @@ }, { "name": "open-telemetry/api", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6" + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/542064815d38a6df55af7957cd6f1d7d967c99c6", - "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", "shasum": "" }, "require": { @@ -488,13 +488,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.1.x-dev" - }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] + }, + "branch-alias": { + "dev-main": "1.1.x-dev" } }, "autoload": { @@ -531,7 +531,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-10-15T22:42:37+00:00" + "time": "2024-11-16T04:32:30+00:00" }, { "name": "open-telemetry/context", @@ -758,13 +758,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.0.x-dev" - }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] + }, + "branch-alias": { + "dev-main": "1.0.x-dev" } }, "autoload": { @@ -1391,16 +1391,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -1438,7 +1438,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -1454,30 +1454,31 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/http-client", - "version": "v7.1.8", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a" + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", - "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ff4df2b68d1c67abb9fef146e6540ea16b58d99e", + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { + "amphp/amp": "<2.5", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -1488,14 +1489,14 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", @@ -1532,7 +1533,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.1.8" + "source": "https://github.com/symfony/http-client/tree/v7.2.1" }, "funding": [ { @@ -1548,20 +1549,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:40:27+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -1569,12 +1570,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1610,7 +1611,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -1626,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1654,8 +1655,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1728,8 +1729,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1808,8 +1809,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1866,16 +1867,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -1929,7 +1930,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -1945,7 +1946,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "tbachert/spi", @@ -1974,10 +1975,10 @@ }, "type": "composer-plugin", "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", "branch-alias": { "dev-main": "0.2.x-dev" }, - "class": "Nevay\\SPI\\Composer\\Plugin", "plugin-optional": true }, "autoload": { @@ -2097,16 +2098,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.14", + "version": "0.33.15", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "45a5a2db3602fa054096f378482c7da9936f5850" + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/45a5a2db3602fa054096f378482c7da9936f5850", - "reference": "45a5a2db3602fa054096f378482c7da9936f5850", + "url": "https://api.github.com/repos/utopia-php/http/zipball/83b0628900c2c53e8c3efbf069f3e13050295edc", + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc", "shasum": "" }, "require": { @@ -2138,9 +2139,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.14" + "source": "https://github.com/utopia-php/http/tree/0.33.15" }, - "time": "2024-11-20T12:39:10+00:00" + "time": "2024-12-10T13:07:04+00:00" }, { "name": "utopia-php/mongo", @@ -2389,16 +2390,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.2", + "version": "v1.18.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64" + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", - "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", + "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", "shasum": "" }, "require": { @@ -2409,13 +2410,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.64.0", - "illuminate/view": "^10.48.20", - "larastan/larastan": "^2.9.8", + "friendsofphp/php-cs-fixer": "^3.65.0", + "illuminate/view": "^10.48.24", + "larastan/larastan": "^2.9.11", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.1" + "nunomaduro/termwind": "^1.17.0", + "pestphp/pest": "^2.36.0" }, "bin": [ "builds/pint" @@ -2451,7 +2452,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-20T09:33:46+00:00" + "time": "2024-11-26T15:34:00+00:00" }, { "name": "myclabs/deep-copy", @@ -2723,16 +2724,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.11", + "version": "1.12.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" + "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b469068840cfa031e1deaf2fa1886d00e20680f", + "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f", "shasum": "" }, "require": { @@ -2777,7 +2778,7 @@ "type": "github" } ], - "time": "2024-11-17T14:08:01+00:00" + "time": "2024-12-17T17:00:20+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3100,16 +3101,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -3120,7 +3121,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -3183,7 +3184,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -3199,7 +3200,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -4348,7 +4349,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4356,6 +4357,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } From 57793e0089e6d6c2141ef33b33f04aea8085173e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 18:33:58 +0200 Subject: [PATCH 237/256] Remove comment --- src/Database/Adapter/MariaDB.php | 1 - src/Database/Database.php | 18 ++++-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 104d2aab8..89f02c0b1 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -467,7 +467,6 @@ public function renameAttribute(string $collection, string $old, string $new): b return $this->getPDO() ->prepare($sql) ->execute(); - } catch (PDOException $e) { throw $this->processException($e); } diff --git a/src/Database/Database.php b/src/Database/Database.php index 6d089c4fc..f29d47953 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -7,7 +7,6 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Conflict as ConflictException; -use Utopia\Database\Exception\Dependency as DependencyException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; @@ -2049,17 +2048,16 @@ public function deleteAttribute(string $collection, string $id): bool } /** - * Rename Attribute - * * @param string $collection - * @param string $old Current attribute ID + * @param string $old * @param string $new * @return bool * @throws AuthorizationException * @throws ConflictException - * @throws DatabaseException * @throws DuplicateException + * @throws NotFoundException * @throws StructureException + * @throws DatabaseException */ public function renameAttribute(string $collection, string $old, string $new): bool { @@ -2097,15 +2095,7 @@ public function renameAttribute(string $collection, string $old, string $new): b foreach ($indexes as $index) { $indexAttributes = $index->getAttribute('attributes', []); - foreach ($indexAttributes as $key => $indexAttribute) { - if ($indexAttribute === $old) { - // if($attribute->getAttribute('array', false)){ - // throw new DependencyException("Can't rename attribute because of functional index dependency must drop index first."); - // } - - $indexAttributes[$key] = $new; - } - } + $indexAttributes = \array_map(fn ($attr) => ($attr === $old) ? $new : $attr, $indexAttributes); $index->setAttribute('attributes', $indexAttributes); } From 10809152c77870a79e76f609a1a53400b5ac015e Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 18:36:02 +0200 Subject: [PATCH 238/256] Extra line --- src/Database/Adapter/MariaDB.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 89f02c0b1..868a0918d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -433,7 +433,6 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa return $this->getPDO() ->prepare($sql) ->execute(); - } catch (PDOException $e) { if ($e->getCode() === "42000" && $e->errorInfo[1] === 1091) { return true; From 9ffece83e9fd16f3dd3d5234d01185f81939ac41 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 23 Dec 2024 18:37:57 +0200 Subject: [PATCH 239/256] hints --- src/Database/Database.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index f29d47953..c7635cd33 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2048,16 +2048,17 @@ public function deleteAttribute(string $collection, string $id): bool } /** + * Rename Attribute + * * @param string $collection - * @param string $old + * @param string $old Current attribute ID * @param string $new * @return bool * @throws AuthorizationException * @throws ConflictException + * @throws DatabaseException * @throws DuplicateException - * @throws NotFoundException * @throws StructureException - * @throws DatabaseException */ public function renameAttribute(string $collection, string $old, string $new): bool { From 8ea2a530f79316d54c799e25f6871b3b827f622d Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 24 Dec 2024 09:09:17 +0200 Subject: [PATCH 240/256] deleteIndex adapter before metadata --- src/Database/Database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index ec37547ca..fe79a5719 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2840,14 +2840,14 @@ public function deleteIndex(string $collection, string $id): bool } } + $deleted = $this->adapter->deleteIndex($collection->getId(), $id); + $collection->setAttribute('indexes', \array_values($indexes)); if ($collection->getId() !== self::METADATA) { $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $deleted = $this->adapter->deleteIndex($collection->getId(), $id); - $this->trigger(self::EVENT_INDEX_DELETE, $indexDeleted); return $deleted; From 77756cb0db91f5fef6392766a6f3d23663ef6a62 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 26 Dec 2024 16:41:57 +0200 Subject: [PATCH 241/256] datetime testing --- composer.lock | 173 +++++++++++++++++++------------------ src/Database/Database.php | 5 ++ tests/e2e/Adapter/Base.php | 27 ++++++ 3 files changed, 119 insertions(+), 86 deletions(-) diff --git a/composer.lock b/composer.lock index d08a958c7..a73ee5ada 100644 --- a/composer.lock +++ b/composer.lock @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.28.3", + "version": "v4.29.2", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100" + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c5c311e0f3d89928251ac5a2f0e3db283612c100", - "reference": "c5c311e0f3d89928251ac5a2f0e3db283612c100", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/79aa5014efeeec3d137df5cdb0ae2fc163953945", + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.28.3" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.2" }, - "time": "2024-10-22T22:27:17+00:00" + "time": "2024-12-18T14:11:12+00:00" }, { "name": "jean85/pretty-package-versions", @@ -465,16 +465,16 @@ }, { "name": "open-telemetry/api", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6" + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/542064815d38a6df55af7957cd6f1d7d967c99c6", - "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", "shasum": "" }, "require": { @@ -488,13 +488,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.1.x-dev" - }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] + }, + "branch-alias": { + "dev-main": "1.1.x-dev" } }, "autoload": { @@ -531,7 +531,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-10-15T22:42:37+00:00" + "time": "2024-11-16T04:32:30+00:00" }, { "name": "open-telemetry/context", @@ -758,13 +758,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.0.x-dev" - }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" ] + }, + "branch-alias": { + "dev-main": "1.0.x-dev" } }, "autoload": { @@ -1391,16 +1391,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -1438,7 +1438,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -1454,30 +1454,31 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/http-client", - "version": "v7.1.8", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a" + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", - "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ff4df2b68d1c67abb9fef146e6540ea16b58d99e", + "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { + "amphp/amp": "<2.5", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -1488,14 +1489,14 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", @@ -1532,7 +1533,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.1.8" + "source": "https://github.com/symfony/http-client/tree/v7.2.1" }, "funding": [ { @@ -1548,20 +1549,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:40:27+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -1569,12 +1570,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1610,7 +1611,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -1626,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1654,8 +1655,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1728,8 +1729,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1808,8 +1809,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1866,16 +1867,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -1929,7 +1930,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -1945,7 +1946,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "tbachert/spi", @@ -1974,10 +1975,10 @@ }, "type": "composer-plugin", "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", "branch-alias": { "dev-main": "0.2.x-dev" }, - "class": "Nevay\\SPI\\Composer\\Plugin", "plugin-optional": true }, "autoload": { @@ -2097,16 +2098,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.14", + "version": "0.33.15", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "45a5a2db3602fa054096f378482c7da9936f5850" + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/45a5a2db3602fa054096f378482c7da9936f5850", - "reference": "45a5a2db3602fa054096f378482c7da9936f5850", + "url": "https://api.github.com/repos/utopia-php/http/zipball/83b0628900c2c53e8c3efbf069f3e13050295edc", + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc", "shasum": "" }, "require": { @@ -2138,9 +2139,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.14" + "source": "https://github.com/utopia-php/http/tree/0.33.15" }, - "time": "2024-11-20T12:39:10+00:00" + "time": "2024-12-10T13:07:04+00:00" }, { "name": "utopia-php/mongo", @@ -2389,16 +2390,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.2", + "version": "v1.18.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64" + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", - "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64", + "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", + "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", "shasum": "" }, "require": { @@ -2409,13 +2410,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.64.0", - "illuminate/view": "^10.48.20", - "larastan/larastan": "^2.9.8", + "friendsofphp/php-cs-fixer": "^3.65.0", + "illuminate/view": "^10.48.24", + "larastan/larastan": "^2.9.11", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.1" + "nunomaduro/termwind": "^1.17.0", + "pestphp/pest": "^2.36.0" }, "bin": [ "builds/pint" @@ -2451,7 +2452,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-20T09:33:46+00:00" + "time": "2024-11-26T15:34:00+00:00" }, { "name": "myclabs/deep-copy", @@ -2723,16 +2724,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.11", + "version": "1.12.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" + "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b469068840cfa031e1deaf2fa1886d00e20680f", + "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f", "shasum": "" }, "require": { @@ -2777,7 +2778,7 @@ "type": "github" } ], - "time": "2024-11-17T14:08:01+00:00" + "time": "2024-12-17T17:00:20+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3100,16 +3101,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -3120,7 +3121,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -3183,7 +3184,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -3199,7 +3200,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -4348,7 +4349,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4356,6 +4357,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/Database/Database.php b/src/Database/Database.php index 730c37581..2c07642c1 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5967,11 +5967,16 @@ public function getLimitForIndexes(): int * @param array $queries * @return array * @throws QueryException + * @throws Exception */ public static function convertQueries(Document $collection, array $queries): array { $attributes = $collection->getAttribute('attributes', []); + foreach (Database::INTERNAL_ATTRIBUTES as $attribute) { + $attributes[] = new Document($attribute); + } + foreach ($attributes as $attribute) { foreach ($queries as $query) { if ($query->getAttribute() === $attribute->getId()) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 19ca3602d..e81f264de 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -6444,6 +6444,16 @@ public function testCreateDatetime(): void ]; foreach ($invalidDates as $date) { + try { + static::getDatabase()->find('datetime', [ + Query::equal('$createdAt', [$date]) + ]); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertTrue($e instanceof QueryException); + $this->assertEquals('Invalid query: Query value is invalid for attribute "$createdAt"', $e->getMessage()); + } + try { static::getDatabase()->find('datetime', [ Query::equal('date', [$date]) @@ -6454,6 +6464,23 @@ public function testCreateDatetime(): void $this->assertEquals('Invalid query: Query value is invalid for attribute "date"', $e->getMessage()); } } + + $validDates = [ + '2024-12-2509:00:21.891119', + 'Tue Dec 31 2024', + ]; + + foreach ($validDates as $date) { + $docs = static::getDatabase()->find('datetime', [ + Query::equal('$createdAt', [$date]) + ]); + $this->assertCount(0, $docs); + + $docs = static::getDatabase()->find('datetime', [ + Query::equal('date', [$date]) + ]); + $this->assertCount(0, $docs); + } } public function testCreateDateTimeAttributeFailure(): void From 4cb6368dd2cb1591a00ed7eb067f8521ff04f185 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 8 Jan 2025 14:26:57 +1300 Subject: [PATCH 242/256] Update tests/e2e/Adapter/Base.php --- tests/e2e/Adapter/Base.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index d16f9630d..7aead1761 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2758,7 +2758,6 @@ public function testArrayAttribute(): void array: true )); - $this->assertEquals(true, $database->createAttribute( $collection, 'names', From 07b46fd9db813342ae35dd1fcc15e97acd3fe3c6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 15:02:42 +1300 Subject: [PATCH 243/256] Override messages for processed exceptions --- composer.json | 6 +- composer.lock | 108 +++++++++++++++--------------- src/Database/Adapter/MariaDB.php | 16 ++--- src/Database/Adapter/Mongo.php | 2 +- src/Database/Adapter/MySQL.php | 4 +- src/Database/Adapter/Postgres.php | 8 +-- src/Database/Adapter/SQLite.php | 4 +- 7 files changed, 74 insertions(+), 74 deletions(-) diff --git a/composer.json b/composer.json index 71c11264f..722e4e5e5 100755 --- a/composer.json +++ b/composer.json @@ -33,9 +33,9 @@ "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, "require": { + "php": ">=8.3", "ext-pdo": "*", "ext-mbstring": "*", - "php": ">=8.3", "utopia-php/framework": "0.33.*", "utopia-php/cache": "0.11.*", "utopia-php/mongo": "0.3.*" @@ -58,8 +58,8 @@ }, "config": { "allow-plugins": { - "php-http/discovery": false, - "tbachert/spi": false + "php-http/discovery": true, + "tbachert/spi": true } } } diff --git a/composer.lock b/composer.lock index a73ee5ada..c1ce86a9b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f7eec4bad737b741ae97c81db0532d29", + "content-hash": "43c3ff88660f90baf3ddf7ec8bc5136d", "packages": [ { "name": "brick/math", @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.29.2", + "version": "v4.29.3", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945" + "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/79aa5014efeeec3d137df5cdb0ae2fc163953945", - "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", + "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.3" }, - "time": "2024-12-18T14:11:12+00:00" + "time": "2025-01-08T21:00:13+00:00" }, { "name": "jean85/pretty-package-versions", @@ -465,16 +465,16 @@ }, { "name": "open-telemetry/api", - "version": "1.1.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed" + "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", - "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/351a30baa79699de3de3a814c8ccc7b52ccdfb1d", + "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d", "shasum": "" }, "require": { @@ -531,7 +531,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-11-16T04:32:30+00:00" + "time": "2025-01-08T23:50:34+00:00" }, { "name": "open-telemetry/context", @@ -594,16 +594,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18" + "reference": "243d9657c44a06f740cf384f486afe954c2b725f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/9b6de12204f25f8ab9540b46d6e7b5151897ce18", - "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/243d9657c44a06f740cf384f486afe954c2b725f", + "reference": "243d9657c44a06f740cf384f486afe954c2b725f", "shasum": "" }, "require": { @@ -654,7 +654,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-04-30T18:28:30+00:00" + "time": "2025-01-08T23:50:03+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -721,16 +721,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.1.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b" + "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/fb0ff8d8279a3776bd604791e2531dd0cc147e8b", - "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/9a1c3b866239dbff291e5cc555bb7793eab08127", + "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127", "shasum": "" }, "require": { @@ -807,7 +807,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-10-18T21:01:35+00:00" + "time": "2025-01-08T23:50:34+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1408,12 +1408,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1458,16 +1458,16 @@ }, { "name": "symfony/http-client", - "version": "v7.2.1", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e" + "reference": "339ba21476eb184290361542f732ad12c97591ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/ff4df2b68d1c67abb9fef146e6540ea16b58d99e", - "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e", + "url": "https://api.github.com/repos/symfony/http-client/zipball/339ba21476eb184290361542f732ad12c97591ec", + "reference": "339ba21476eb184290361542f732ad12c97591ec", "shasum": "" }, "require": { @@ -1533,7 +1533,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.1" + "source": "https://github.com/symfony/http-client/tree/v7.2.2" }, "funding": [ { @@ -1549,7 +1549,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:50:44+00:00" + "time": "2024-12-30T18:35:15+00:00" }, { "name": "symfony/http-client-contracts", @@ -1889,12 +1889,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -2390,16 +2390,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.3", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", + "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0", + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0", "shasum": "" }, "require": { @@ -2410,10 +2410,10 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.65.0", - "illuminate/view": "^10.48.24", - "larastan/larastan": "^2.9.11", - "laravel-zero/framework": "^10.4.0", + "friendsofphp/php-cs-fixer": "^3.66.0", + "illuminate/view": "^10.48.25", + "larastan/larastan": "^2.9.12", + "laravel-zero/framework": "^10.48.25", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.17.0", "pestphp/pest": "^2.36.0" @@ -2452,7 +2452,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-26T15:34:00+00:00" + "time": "2024-12-30T16:20:10+00:00" }, { "name": "myclabs/deep-copy", @@ -2724,16 +2724,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.13", + "version": "1.12.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f" + "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b469068840cfa031e1deaf2fa1886d00e20680f", - "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c91d4e8bc056f46cf653656e6f71004b254574d1", + "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1", "shasum": "" }, "require": { @@ -2778,7 +2778,7 @@ "type": "github" } ], - "time": "2024-12-17T17:00:20+00:00" + "time": "2025-01-05T16:40:22+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4349,14 +4349,14 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { + "php": ">=8.3", "ext-pdo": "*", - "ext-mbstring": "*", - "php": ">=8.3" + "ext-mbstring": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e832776e4..f8c1cf60d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1654,8 +1654,8 @@ public function deleteDocument(string $collection, string $id): bool $name = $this->filter($collection); $sql = " - DELETE FROM {$this->getSQLTable($name)} - WHERE _uid = :_uid + DELETE FROM {$this->getSQLTable($name)} + WHERE _uid = :_uid "; if ($this->sharedTables) { @@ -2394,27 +2394,27 @@ protected function processException(PDOException $e): \Exception { // Timeout if ($e->getCode() === '70100' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1969) { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); + return new TimeoutException('Query timed out', $e->getCode(), $e); } // Duplicate table if ($e->getCode() === '42S01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1050) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Collection already exists', $e->getCode(), $e); } // Duplicate column if ($e->getCode() === '42S21' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1060) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Attribute already exists', $e->getCode(), $e); } // Duplicate index if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1061) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Index already exists', $e->getCode(), $e); } // Duplicate row if ($e->getCode() === '23000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1062) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Document already exists', $e->getCode(), $e); } // Data is too big for column resize @@ -2425,7 +2425,7 @@ protected function processException(PDOException $e): \Exception // Unknown database if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1049) { - return new NotFoundException($e->getMessage(), $e->getCode(), $e); + return new NotFoundException('Database not found', $e->getCode(), $e); } return $e; diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index bae6ca9db..7d6f7ddcb 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1934,7 +1934,7 @@ public function getKeywords(): array protected function processException(Exception $e): \Exception { if ($e->getCode() === 50) { - return new Timeout($e->getMessage()); + return new Timeout('Query timed out', $e->getCode(), $e); } return $e; diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index fb1694cd0..60cd66ab9 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -84,12 +84,12 @@ protected function processException(PDOException $e): \Exception { // Timeout if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); + return new TimeoutException('Query timed out', $e->getCode(), $e); } // Functional index dependency if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3837) { - return new DependencyException($e->errorInfo[2], $e->getCode(), $e); + return new DependencyException('Attribute cannot be deleted because it is used in an index', $e->getCode(), $e); } return parent::processException($e); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index c37c95e0f..9ca80f90c 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2421,22 +2421,22 @@ protected function processException(PDOException $e): \Exception { // Timeout if ($e->getCode() === '57014' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); + return new TimeoutException('Query timed out', $e->getCode(), $e); } // Duplicate table if ($e->getCode() === '42P07' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Collection already exists', $e->getCode(), $e); } // Duplicate column if ($e->getCode() === '42701' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Attribute already exists', $e->getCode(), $e); } // Duplicate row if ($e->getCode() === '23505' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Document already exists', $e->getCode(), $e); } // Data is too big for column resize diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 8442367ff..31bc38f38 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1210,12 +1210,12 @@ protected function processException(PDOException $e): \Exception { // Timeout if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 3024) { - return new TimeoutException($e->getMessage(), $e->getCode(), $e); + return new TimeoutException('Query timed out', $e->getCode(), $e); } // Duplicate if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1) { - return new DuplicateException($e->getMessage(), $e->getCode(), $e); + return new DuplicateException('Document already exists', $e->getCode(), $e); } return $e; From c0b5745e93a7ba34c6d807e60e9a270b324c814a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 15:25:12 +1300 Subject: [PATCH 244/256] Fix tests --- tests/e2e/Adapter/Base.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index cde062f7e..2b0bb2420 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1301,7 +1301,7 @@ public function testPurgeCollectionCache(): void $this->assertArrayHasKey('age', $document); } - public function testSchemaAttribute(): void + public function testSchemaAttributes(): void { if (!$this->getDatabase()->getAdapter()->getSupportForSchemaAttributes()) { $this->expectNotToPerformAssertions(); @@ -1358,9 +1358,9 @@ public function testSchemaAttribute(): void if ($db->getSharedTables()) { $attribute = $attributes['_tenant']; $this->assertEquals('_tenant', $attribute['columnName']); - $this->assertEquals('int', $attribute['dataType']); + $this->assertEquals('bigint', $attribute['dataType']); $this->assertEquals('10', $attribute['numericPrecision']); - $this->assertTrue(in_array($attribute['columnType'], ['int unsigned', 'int(11) unsigned'])); + $this->assertEquals('bigint unsigned', $attribute['columnType']); } } @@ -2910,7 +2910,7 @@ public function testArrayAttribute(): void $this->fail('Failed to throw exception'); } catch (Throwable $e) { $this->assertInstanceOf(DependencyException::class, $e); - $this->assertEquals("Column 'cards' has a functional index dependency and cannot be dropped or renamed.", $e->getMessage()); + $this->assertEquals('Attribute cannot be deleted because it is used in an index', $e->getMessage()); } try { @@ -2918,7 +2918,7 @@ public function testArrayAttribute(): void $this->fail('Failed to throw exception'); } catch (Throwable $e) { $this->assertInstanceOf(DependencyException::class, $e); - $this->assertEquals("Column 'cards' has a functional index dependency and cannot be dropped or renamed.", $e->getMessage()); + $this->assertEquals('Attribute cannot be deleted because it is used in an index', $e->getMessage()); } } else { $this->assertTrue($database->renameAttribute($collection, 'cards', 'cards_new')); From fc2ab1b793f3710beabdb8cad563204f33a0a656 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 15:51:55 +1300 Subject: [PATCH 245/256] Fix test --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 2b0bb2420..b064b4864 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1359,7 +1359,7 @@ public function testSchemaAttributes(): void $attribute = $attributes['_tenant']; $this->assertEquals('_tenant', $attribute['columnName']); $this->assertEquals('bigint', $attribute['dataType']); - $this->assertEquals('10', $attribute['numericPrecision']); + $this->assertEquals('20', $attribute['numericPrecision']); $this->assertEquals('bigint unsigned', $attribute['columnType']); } } From 623108c19624472579f4b544023714dc82db2f95 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 16:07:37 +1300 Subject: [PATCH 246/256] Fix test --- tests/e2e/Adapter/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index b064b4864..cb16877e8 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1360,7 +1360,7 @@ public function testSchemaAttributes(): void $this->assertEquals('_tenant', $attribute['columnName']); $this->assertEquals('bigint', $attribute['dataType']); $this->assertEquals('20', $attribute['numericPrecision']); - $this->assertEquals('bigint unsigned', $attribute['columnType']); + $this->assertTrue(in_array($attribute['columnType'], ['bigint unsigned', 'bigint(20) unsigned'])); } } From 0e6b2e750692d933c77acdc2e021b9458dbf09d0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 19:33:12 +1300 Subject: [PATCH 247/256] Fix indexed query validation not checking nested queries --- composer.lock | 102 +++++++++++----------- src/Database/Validator/IndexedQueries.php | 16 +++- tests/e2e/Adapter/Base.php | 41 +++++++++ 3 files changed, 106 insertions(+), 53 deletions(-) diff --git a/composer.lock b/composer.lock index a73ee5ada..ac9f2f677 100644 --- a/composer.lock +++ b/composer.lock @@ -149,16 +149,16 @@ }, { "name": "google/protobuf", - "version": "v4.29.2", + "version": "v4.29.3", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945" + "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/79aa5014efeeec3d137df5cdb0ae2fc163953945", - "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", + "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", "shasum": "" }, "require": { @@ -187,9 +187,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.3" }, - "time": "2024-12-18T14:11:12+00:00" + "time": "2025-01-08T21:00:13+00:00" }, { "name": "jean85/pretty-package-versions", @@ -465,16 +465,16 @@ }, { "name": "open-telemetry/api", - "version": "1.1.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed" + "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", - "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/351a30baa79699de3de3a814c8ccc7b52ccdfb1d", + "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d", "shasum": "" }, "require": { @@ -531,7 +531,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-11-16T04:32:30+00:00" + "time": "2025-01-08T23:50:34+00:00" }, { "name": "open-telemetry/context", @@ -594,16 +594,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18" + "reference": "243d9657c44a06f740cf384f486afe954c2b725f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/9b6de12204f25f8ab9540b46d6e7b5151897ce18", - "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/243d9657c44a06f740cf384f486afe954c2b725f", + "reference": "243d9657c44a06f740cf384f486afe954c2b725f", "shasum": "" }, "require": { @@ -654,7 +654,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-04-30T18:28:30+00:00" + "time": "2025-01-08T23:50:03+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -721,16 +721,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.1.2", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b" + "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/fb0ff8d8279a3776bd604791e2531dd0cc147e8b", - "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/9a1c3b866239dbff291e5cc555bb7793eab08127", + "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127", "shasum": "" }, "require": { @@ -807,7 +807,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-10-18T21:01:35+00:00" + "time": "2025-01-08T23:50:34+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1408,12 +1408,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1458,16 +1458,16 @@ }, { "name": "symfony/http-client", - "version": "v7.2.1", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e" + "reference": "339ba21476eb184290361542f732ad12c97591ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/ff4df2b68d1c67abb9fef146e6540ea16b58d99e", - "reference": "ff4df2b68d1c67abb9fef146e6540ea16b58d99e", + "url": "https://api.github.com/repos/symfony/http-client/zipball/339ba21476eb184290361542f732ad12c97591ec", + "reference": "339ba21476eb184290361542f732ad12c97591ec", "shasum": "" }, "require": { @@ -1533,7 +1533,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.1" + "source": "https://github.com/symfony/http-client/tree/v7.2.2" }, "funding": [ { @@ -1549,7 +1549,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:50:44+00:00" + "time": "2024-12-30T18:35:15+00:00" }, { "name": "symfony/http-client-contracts", @@ -1889,12 +1889,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -2390,16 +2390,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.3", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", + "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0", + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0", "shasum": "" }, "require": { @@ -2410,10 +2410,10 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.65.0", - "illuminate/view": "^10.48.24", - "larastan/larastan": "^2.9.11", - "laravel-zero/framework": "^10.4.0", + "friendsofphp/php-cs-fixer": "^3.66.0", + "illuminate/view": "^10.48.25", + "larastan/larastan": "^2.9.12", + "laravel-zero/framework": "^10.48.25", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.17.0", "pestphp/pest": "^2.36.0" @@ -2452,7 +2452,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-26T15:34:00+00:00" + "time": "2024-12-30T16:20:10+00:00" }, { "name": "myclabs/deep-copy", @@ -2724,16 +2724,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.13", + "version": "1.12.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f" + "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b469068840cfa031e1deaf2fa1886d00e20680f", - "reference": "9b469068840cfa031e1deaf2fa1886d00e20680f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c91d4e8bc056f46cf653656e6f71004b254574d1", + "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1", "shasum": "" }, "require": { @@ -2778,7 +2778,7 @@ "type": "github" } ], - "time": "2024-12-17T17:00:20+00:00" + "time": "2025-01-05T16:40:22+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4349,7 +4349,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4357,6 +4357,6 @@ "ext-mbstring": "*", "php": ">=8.3" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/Database/Validator/IndexedQueries.php b/src/Database/Validator/IndexedQueries.php index 24846460e..cb727c0fb 100644 --- a/src/Database/Validator/IndexedQueries.php +++ b/src/Database/Validator/IndexedQueries.php @@ -68,8 +68,20 @@ public function isValid($value): bool } $queries = []; foreach ($value as $query) { - if (!$query instanceof Query) { - $query = Query::parse($query); + if (! $query instanceof Query) { + try { + $query = Query::parse($query); + } catch (\Throwable $e) { + $this->message = 'Invalid query: '.$e->getMessage(); + + return false; + } + } + + if ($query->isNested()) { + if (! self::isValid($query->getValues())) { + return false; + } } $queries[] = $query; diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index cde062f7e..bf8e694c2 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -17165,6 +17165,47 @@ public function testUpdateDocumentsRelationships(): void } } + public function testNestedQueryValidation(): void + { + $this->getDatabase()->createCollection(__FUNCTION__, [ + new Document([ + '$id' => ID::custom('name'), + 'type' => Database::VAR_STRING, + 'size' => 255, + 'required' => true, + ]) + ], permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ]); + + $this->getDatabase()->createDocuments(__FUNCTION__, [ + new Document([ + '$id' => ID::unique(), + 'name' => 'test1', + ]), + new Document([ + '$id' => ID::unique(), + 'name' => 'doc2', + ]), + ]); + + try { + $this->getDatabase()->find(__FUNCTION__, [ + Query::or([ + Query::equal('name', ['test1']), + Query::search('name', 'doc'), + ]) + ]); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(QueryException::class, $e); + $this->assertEquals('Searching by attribute "name" requires a fulltext index.', $e->getMessage()); + } + } + public function testEvents(): void { Authorization::skip(function () { From 67e237c7763b279c7bbd833fc560271cba9499d1 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 20:50:18 +1300 Subject: [PATCH 248/256] Revert "Primary key bigint" --- src/Database/Adapter/MariaDB.php | 52 +++++++++++++++---------------- src/Database/Adapter/Postgres.php | 8 ++--- src/Database/Adapter/SQL.php | 4 +-- src/Database/Validator/Index.php | 7 ----- 4 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index f8c1cf60d..449ac4708 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -148,49 +148,49 @@ public function createCollection(string $name, array $attributes = [], array $in $indexStrings[$key] = "{$indexType} `{$indexId}` ({$indexAttributes}),"; } + $collection = " + CREATE TABLE {$this->getSQLTable($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, + PRIMARY KEY (_id), + " . \implode(' ', $attributeStrings) . " + " . \implode(' ', $indexStrings) . " + "; + if ($this->sharedTables) { - $indexSql = ' - _tenant BIGINT UNSIGNED DEFAULT NULL, - UNIQUE KEY (_uid, _tenant), + $collection .= " + _tenant INT(11) UNSIGNED DEFAULT NULL, + UNIQUE KEY _uid (_uid, _tenant), KEY _created_at (_tenant, _createdAt), KEY _updated_at (_tenant, _updatedAt), - KEY _tenant_id (_tenant, _id), - '; + KEY _tenant_id (_tenant, _id) + "; } else { - $indexSql = ' + $collection .= " UNIQUE KEY _uid (_uid), KEY _created_at (_createdAt), - KEY _updated_at (_updatedAt), - '; + KEY _updated_at (_updatedAt) + "; } - $collection = " - CREATE TABLE {$this->getSQLTable($id)} ( - `_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - `_uid` VARCHAR(255) NOT NULL, - `_createdAt` DATETIME(3) DEFAULT NULL, - `_updatedAt` DATETIME(3) DEFAULT NULL, - `_permissions` MEDIUMTEXT DEFAULT NULL, - ".$indexSql." - ". \implode(' ', $attributeStrings) . " - ". \implode(' ', $indexStrings) . " - PRIMARY KEY (`_id`) - )"; - + $collection .= ")"; $collection = $this->trigger(Database::EVENT_COLLECTION_CREATE, $collection); $permissions = " CREATE TABLE {$this->getSQLTable($id . '_perms')} ( - `_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - `_type` VARCHAR(12) NOT NULL, - `_permission` VARCHAR(255) NOT NULL, - `_document` VARCHAR(255) NOT NULL, + _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), "; if ($this->sharedTables) { $permissions .= " - _tenant BIGINT UNSIGNED DEFAULT NULL, + _tenant INT(11) UNSIGNED DEFAULT NULL, UNIQUE INDEX _index1 (_document, _tenant, _type, _permission), INDEX _permission (_tenant, _permission, _type) "; diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 9ca80f90c..50862c620 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -197,11 +197,11 @@ public function createCollection(string $name, array $attributes = [], array $in $attributeStrings[] = "\"{$attrId}\" {$attrType}, "; } - $sqlTenant = $this->sharedTables ? '_tenant BIGINT DEFAULT NULL,' : ''; + $sqlTenant = $this->sharedTables ? '_tenant INTEGER DEFAULT NULL,' : ''; $collection = " CREATE TABLE {$this->getSQLTable($id)} ( - _id BIGSERIAL NOT NULL, + _id SERIAL NOT NULL, _uid VARCHAR(255) NOT NULL, ". $sqlTenant ." \"_createdAt\" TIMESTAMP(3) DEFAULT NULL, @@ -231,8 +231,8 @@ public function createCollection(string $name, array $attributes = [], array $in $permissions = " CREATE TABLE {$this->getSQLTable($id . '_perms')} ( - _id BIGSERIAL NOT NULL, - _tenant BIGINT DEFAULT NULL, + _id SERIAL NOT NULL, + _tenant INTEGER DEFAULT NULL, _type VARCHAR(12) NOT NULL, _permission VARCHAR(255) NOT NULL, _document VARCHAR(255) NOT NULL, diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index eb56e0082..725a91a50 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -1101,9 +1101,9 @@ public function getMaxVarcharLength(): int public function getMaxIndexLength(): int { /** - * $tenant bigint requires 2 index length + * $tenant int = 1 */ - return $this->sharedTables ? 766 : 768; + return $this->sharedTables ? 767 : 768; } /** diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index 0357a5abb..2270a6e0f 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -185,17 +185,10 @@ public function checkIndexLength(Document $index): bool $attributeSize = $attribute->getAttribute('size', 0); $indexLength = $lengths[$attributePosition] ?? $attributeSize; break; - case Database::VAR_FLOAT: $attributeSize = 2; // 8 bytes / 4 mb4 $indexLength = 2; break; - - case Database::VAR_INTEGER: - $attributeSize = $attribute->getAttribute('size', 0); - $indexLength = $attributeSize = $attributeSize >= 8 ? 2 : 1; // bigint - break; - default: $attributeSize = 1; // 4 bytes / 4 mb4 $indexLength = 1; From 6491885e7ba404fd38db36630cc0f37c754bbe2d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 10 Jan 2025 21:45:05 +1300 Subject: [PATCH 249/256] FIx tests --- tests/e2e/Adapter/Base.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index ea04be302..90298d472 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1358,9 +1358,9 @@ public function testSchemaAttributes(): void if ($db->getSharedTables()) { $attribute = $attributes['_tenant']; $this->assertEquals('_tenant', $attribute['columnName']); - $this->assertEquals('bigint', $attribute['dataType']); - $this->assertEquals('20', $attribute['numericPrecision']); - $this->assertTrue(in_array($attribute['columnType'], ['bigint unsigned', 'bigint(20) unsigned'])); + $this->assertEquals('int', $attribute['dataType']); + $this->assertEquals('10', $attribute['numericPrecision']); + $this->assertTrue(in_array($attribute['columnType'], ['int unsigned', 'int(11) unsigned'])); } } From af2d2f46727fd426fea2b3cdd1effde62138c4aa Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:05:42 +1300 Subject: [PATCH 250/256] Revert "Merge pull request #376 from utopia-php/feat-framework-v2" --- README.md | 2 +- bin/tasks/index.php | 2 +- bin/tasks/load.php | 19 +- bin/tasks/query.php | 24 +- composer.json | 4 +- composer.lock | 2174 ++++++++-- src/Database/Adapter.php | 32 +- src/Database/Adapter/MariaDB.php | 31 +- src/Database/Adapter/Mongo.php | 19 +- src/Database/Adapter/Postgres.php | 25 +- src/Database/Adapter/SQL.php | 6 +- src/Database/Adapter/SQLite.php | 8 +- src/Database/Database.php | 140 +- src/Database/Exception.php | 2 +- src/Database/Query.php | 4 +- src/Database/Validator/Authorization.php | 147 +- .../Validator/Authorization/Input.php | 49 - src/Database/Validator/Datetime.php | 20 +- src/Database/Validator/Index.php | 16 +- src/Database/Validator/Key.php | 2 +- src/Database/Validator/Queries.php | 6 +- src/Database/Validator/Query/Base.php | 2 +- src/Database/Validator/Query/Filter.php | 26 +- src/Database/Validator/Query/Limit.php | 4 +- src/Database/Validator/Query/Offset.php | 4 +- src/Database/Validator/Roles.php | 2 +- src/Database/Validator/Structure.php | 16 +- tests/e2e/Adapter/Base.php | 3578 +++++++++-------- tests/e2e/Adapter/MariaDBTest.php | 8 +- tests/e2e/Adapter/MongoDBTest.php | 16 +- tests/e2e/Adapter/MySQLTest.php | 8 +- tests/e2e/Adapter/PostgresTest.php | 8 +- tests/e2e/Adapter/SQLiteTest.php | 8 +- .../e2e/Adapter/SharedTables/MariaDBTest.php | 3 +- .../e2e/Adapter/SharedTables/MongoDBTest.php | 3 +- tests/e2e/Adapter/SharedTables/MySQLTest.php | 3 +- .../e2e/Adapter/SharedTables/PostgresTest.php | 3 +- tests/e2e/Adapter/SharedTables/SQLiteTest.php | 3 +- tests/unit/Format.php | 5 +- tests/unit/Validator/AuthorizationTest.php | 94 +- tests/unit/Validator/DateTimeTest.php | 2 +- 41 files changed, 4047 insertions(+), 2481 deletions(-) delete mode 100644 src/Database/Validator/Authorization/Input.php diff --git a/README.md b/README.md index cccfa1afa..1adf58143 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A list of the utopia/php concepts and their relevant equivalent using the differ - **Document** - A simple JSON object that will be stored in one of the utopia/database collections. For SQL-based adapters, this will be equivalent to a row. For a No-SQL adapter, this will equivalent to a native document. - **Attribute** - A simple document attribute. For SQL-based adapters, this will be equivalent to a column. For a No-SQL adapter, this will equivalent to a native document field. - **Index** - A simple collection index used to improve the performance of your database queries. -- **Permissions** - Using permissions, you can decide which roles have read, create, update and delete access for a specific document. The special attribute `$permissions` is used to store permission metadata for each document in the collection. A permission role can be any string you want. You can use `$authorization->addRole()` to delegate new roles to your users, once obtained a new role a user would gain read, create, update or delete access to a relevant document. +- **Permissions** - Using permissions, you can decide which roles have read, create, update and delete access for a specific document. The special attribute `$permissions` is used to store permission metadata for each document in the collection. A permission role can be any string you want. You can use `Authorization::setRole()` to delegate new roles to your users, once obtained a new role a user would gain read, create, update or delete access to a relevant document. ### Filters diff --git a/bin/tasks/index.php b/bin/tasks/index.php index c1505cc26..5b43b5920 100644 --- a/bin/tasks/index.php +++ b/bin/tasks/index.php @@ -13,8 +13,8 @@ use Utopia\Database\Adapter\Mongo; use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; -use Utopia\Http\Validator\Text; use Utopia\Mongo\Client; +use Utopia\Validator\Text; /** * @Example diff --git a/bin/tasks/load.php b/bin/tasks/load.php index e48a4cc55..31deb39dc 100644 --- a/bin/tasks/load.php +++ b/bin/tasks/load.php @@ -21,11 +21,9 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; -use Utopia\Http\Validator\Numeric; -use Utopia\Http\Validator\Text; use Utopia\Mongo\Client; - -$authorization = new Authorization(); +use Utopia\Validator\Numeric; +use Utopia\Validator\Text; /** * @Example @@ -63,7 +61,7 @@ $database->setNamespace($namespace); // Outline collection schema - $createSchema($database); + createSchema($database); // reclaim resources $database = null; @@ -123,7 +121,7 @@ $database->setNamespace($namespace); // Outline collection schema - $createSchema($database); + createSchema($database); // reclaim resources $database = null; @@ -185,7 +183,7 @@ $database->setNamespace($namespace); // Outline collection schema - $createSchema($database); + createSchema($database); // Fill DB $faker = Factory::create(); @@ -228,13 +226,14 @@ }); -$createSchema = function (Database $database) use ($authorization): void { +function createSchema(Database $database): void +{ if ($database->exists($database->getDatabase())) { $database->delete($database->getDatabase()); } $database->create(); - $authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); $database->createCollection('articles', permissions: [ Permission::create(Role::any()), @@ -248,7 +247,7 @@ $database->createAttribute('articles', 'views', Database::VAR_INTEGER, 0, true); $database->createAttribute('articles', 'tags', Database::VAR_STRING, 0, true, array: true); $database->createIndex('articles', 'text', Database::INDEX_FULLTEXT, ['text']); -}; +} function createDocument($database, Generator $faker): void { diff --git a/bin/tasks/query.php b/bin/tasks/query.php index 28dc0c431..ed84fd00c 100644 --- a/bin/tasks/query.php +++ b/bin/tasks/query.php @@ -14,9 +14,10 @@ use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; use Utopia\Database\Query; -use Utopia\Http\Validator\Numeric; -use Utopia\Http\Validator\Text; +use Utopia\Database\Validator\Authorization; use Utopia\Mongo\Client; +use Utopia\Validator\Numeric; +use Utopia\Validator\Text; /** * @Example @@ -83,35 +84,35 @@ $report = []; - $count = $setRoles($faker, 1); + $count = setRoles($faker, 1); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = $setRoles($faker, 100); + $count = setRoles($faker, 100); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = $setRoles($faker, 400); + $count = setRoles($faker, 400); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = $setRoles($faker, 500); + $count = setRoles($faker, 500); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, 'results' => runQueries($database, $limit) ]; - $count = $setRoles($faker, 1000); + $count = setRoles($faker, 1000); Console::info("\n{$count} roles:"); $report[] = [ 'roles' => $count, @@ -135,12 +136,13 @@ Console::error($error->getMessage()); }); -$setRoles = function ($faker, $count) use ($authorization): int { +function setRoles($faker, $count): int +{ for ($i = 0; $i < $count; $i++) { - $authorization->addRole($faker->numerify('user####')); + Authorization::setRole($faker->numerify('user####')); } - return \count($authorization->getRoles()); -}; + return \count(Authorization::getRoles()); +} function runQueries(Database $database, int $limit): array { diff --git a/composer.json b/composer.json index de12d86f6..a99e03468 100755 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "ext-pdo": "*", "ext-mbstring": "*", "php": ">=8.0", - "utopia-php/framework": "1.0.*", + "utopia-php/framework": "0.33.*", "utopia-php/cache": "0.10.*", "utopia-php/mongo": "0.3.*" }, @@ -45,7 +45,7 @@ "phpunit/phpunit": "9.6.*", "pcov/clobber": "2.0.*", "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "0.19.*", + "utopia-php/cli": "0.14.*", "laravel/pint": "1.17.*", "phpstan/phpstan": "1.11.*", "rregeer/phpunit-coverage-check": "0.3.*" diff --git a/composer.lock b/composer.lock index 2ad152cc9..2021f0b9e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,32 +4,217 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "62684826df21956b2d48dc2c42cc3e07", + "content-hash": "8a6537ec5c4f47cd0b6c34e9ceda042b", "packages": [ + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "google/protobuf", + "version": "v4.29.3", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", + "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": ">=5.0.0" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.3" + }, + "time": "2025-01-08T21:00:13+00:00" + }, { "name": "jean85/pretty-package-versions", - "version": "2.0.6", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", "shasum": "" }, "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", "extra": { @@ -61,9 +246,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" }, - "time": "2024-03-08T09:58:59+00:00" + "time": "2024-11-18T16:19:46+00:00" }, { "name": "mongodb/mongodb", @@ -100,73 +285,1624 @@ }, "autoload": { "files": [ - "src/functions.php" + "src/functions.php" + ], + "psr-4": { + "MongoDB\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Andreas Braun", + "email": "andreas.braun@mongodb.com" + }, + { + "name": "Jeremy Mikola", + "email": "jmikola@gmail.com" + } + ], + "description": "MongoDB driver library", + "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "keywords": [ + "database", + "driver", + "mongodb", + "persistence" + ], + "support": { + "issues": "https://github.com/mongodb/mongo-php-library/issues", + "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" + }, + "time": "2021-10-20T22:22:37+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-08T09:30:43+00:00" + }, + { + "name": "open-telemetry/api", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/351a30baa79699de3de3a814c8ccc7b52ccdfb1d", + "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d", + "shasum": "" + }, + "require": { + "open-telemetry/context": "^1.0", + "php": "^8.1", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/polyfill-php82": "^1.26" + }, + "conflict": { + "open-telemetry/sdk": "<=1.0.8" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.1.x-dev" + } + }, + "autoload": { + "files": [ + "Trace/functions.php" + ], + "psr-4": { + "OpenTelemetry\\API\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "API for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "api", + "apm", + "logging", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-01-08T23:50:34+00:00" + }, + { + "name": "open-telemetry/context", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/context.git", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-php82": "^1.26" + }, + "suggest": { + "ext-ffi": "To allow context switching in Fibers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "fiber/initialize_fiber_handler.php" + ], + "psr-4": { + "OpenTelemetry\\Context\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Context implementation for OpenTelemetry PHP.", + "keywords": [ + "Context", + "opentelemetry", + "otel" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-08-21T00:29:20+00:00" + }, + { + "name": "open-telemetry/exporter-otlp", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/exporter-otlp.git", + "reference": "243d9657c44a06f740cf384f486afe954c2b725f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/243d9657c44a06f740cf384f486afe954c2b725f", + "reference": "243d9657c44a06f740cf384f486afe954c2b725f", + "shasum": "" + }, + "require": { + "open-telemetry/api": "^1.0", + "open-telemetry/gen-otlp-protobuf": "^1.1", + "open-telemetry/sdk": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Otlp\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "OTLP exporter for OpenTelemetry.", + "keywords": [ + "Metrics", + "exporter", + "gRPC", + "http", + "opentelemetry", + "otel", + "otlp", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-01-08T23:50:03+00:00" + }, + { + "name": "open-telemetry/gen-otlp-protobuf", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", + "reference": "66c3b98e998a726691c92e6405a82e6e7b8b169d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/66c3b98e998a726691c92e6405a82e6e7b8b169d", + "reference": "66c3b98e998a726691c92e6405a82e6e7b8b169d", + "shasum": "" + }, + "require": { + "google/protobuf": "^3.22 || ^4.0", + "php": "^8.0" + }, + "suggest": { + "ext-protobuf": "For better performance, when dealing with the protobuf format" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opentelemetry\\Proto\\": "Opentelemetry/Proto/", + "GPBMetadata\\Opentelemetry\\": "GPBMetadata/Opentelemetry/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "PHP protobuf files for communication with OpenTelemetry OTLP collectors/servers.", + "keywords": [ + "Metrics", + "apm", + "gRPC", + "logging", + "opentelemetry", + "otel", + "otlp", + "protobuf", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-10-30T11:49:49+00:00" + }, + { + "name": "open-telemetry/sdk", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sdk.git", + "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/9a1c3b866239dbff291e5cc555bb7793eab08127", + "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nyholm/psr7-server": "^1.1", + "open-telemetry/api": "~1.0 || ~1.1", + "open-telemetry/context": "^1.0", + "open-telemetry/sem-conv": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0.1|^2.0", + "psr/log": "^1.1|^2.0|^3.0", + "ramsey/uuid": "^3.0 || ^4.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php82": "^1.26", + "tbachert/spi": "^1.0.1" + }, + "suggest": { + "ext-gmp": "To support unlimited number of synchronous metric readers", + "ext-mbstring": "To increase performance of string operations", + "open-telemetry/sdk-configuration": "File-based OpenTelemetry SDK configuration" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "Common/Util/functions.php", + "Logs/Exporter/_register.php", + "Metrics/MetricExporter/_register.php", + "Propagation/_register.php", + "Trace/SpanExporter/_register.php", + "Common/Dev/Compatibility/_load.php", + "_autoload.php" + ], + "psr-4": { + "OpenTelemetry\\SDK\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "SDK for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "sdk", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2025-01-08T23:50:34+00:00" + }, + { + "name": "open-telemetry/sem-conv", + "version": "1.27.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sem-conv.git", + "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/1dba705fea74bc0718d04be26090e3697e56f4e6", + "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\SemConv\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Semantic conventions for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "semantic conventions", + "semconv", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-08-28T09:20:31+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "339ba21476eb184290361542f732ad12c97591ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/339ba21476eb184290361542f732ad12c97591ec", + "reference": "339ba21476eb184290361542f732ad12c97591ec", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T18:35:15+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:49:48+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" ], "psr-4": { - "MongoDB\\": "src/" - } + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { - "name": "Andreas Braun", - "email": "andreas.braun@mongodb.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Jeremy Mikola", - "email": "jmikola@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "MongoDB driver library", - "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "database", - "driver", - "mongodb", - "persistence" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.31.0" }, - "time": "2021-10-20T22:22:37+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "name": "symfony/service-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Contracts\\Service\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -174,10 +1910,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -187,16 +1919,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Generic abstractions related to writing services", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -212,7 +1946,59 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "tbachert/spi", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Nevay/spi.git", + "reference": "2ddfaf815dafb45791a61b08170de8d583c16062" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062", + "reference": "2ddfaf815dafb45791a61b08170de8d583c16062", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "composer/semver": "^1.0 || ^2.0 || ^3.0", + "php": "^8.1" + }, + "require-dev": { + "composer/composer": "^2.0", + "infection/infection": "^0.27.9", + "phpunit/phpunit": "^10.5", + "psalm/phar": "^5.18" + }, + "type": "composer-plugin", + "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", + "branch-alias": { + "dev-main": "0.2.x-dev" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Nevay\\SPI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Service provider loading facility", + "keywords": [ + "service provider" + ], + "support": { + "issues": "https://github.com/Nevay/spi/issues", + "source": "https://github.com/Nevay/spi/tree/v1.0.2" + }, + "time": "2024-10-04T16:36:12+00:00" }, { "name": "utopia-php/cache", @@ -265,79 +2051,75 @@ "time": "2024-06-25T20:36:35+00:00" }, { - "name": "utopia-php/di", - "version": "0.1.0", + "name": "utopia-php/compression", + "version": "0.1.2", "source": { "type": "git", - "url": "https://github.com/utopia-php/di.git", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" + "url": "https://github.com/utopia-php/compression.git", + "reference": "6062f70596415f8d5de40a589367b0eb2a435f98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", - "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "url": "https://api.github.com/repos/utopia-php/compression/zipball/6062f70596415f8d5de40a589367b0eb2a435f98", + "reference": "6062f70596415f8d5de40a589367b0eb2a435f98", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.0" }, "require-dev": { - "laravel/pint": "^1.2", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25", - "swoole/ide-helper": "4.8.3" + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" }, "type": "library", "autoload": { "psr-4": { - "Utopia\\": "src/", - "Tests\\E2E\\": "tests/e2e" + "Utopia\\Compression\\": "src/Compression" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A simple and lite library for managing dependency injections", + "description": "A simple Compression library to handle file compression", "keywords": [ + "compression", "framework", - "http", "php", - "upf" + "upf", + "utopia" ], "support": { - "issues": "https://github.com/utopia-php/di/issues", - "source": "https://github.com/utopia-php/di/tree/0.1.0" + "issues": "https://github.com/utopia-php/compression/issues", + "source": "https://github.com/utopia-php/compression/tree/0.1.2" }, - "time": "2024-08-08T14:35:19+00:00" + "time": "2024-11-08T14:59:54+00:00" }, { "name": "utopia-php/framework", - "version": "1.0.2", + "version": "0.33.15", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "fc63ec61c720190a5ea5bb484c615145850951e6" + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/fc63ec61c720190a5ea5bb484c615145850951e6", - "reference": "fc63ec61c720190a5ea5bb484c615145850951e6", + "url": "https://api.github.com/repos/utopia-php/http/zipball/83b0628900c2c53e8c3efbf069f3e13050295edc", + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc", "shasum": "" }, "require": { - "ext-swoole": "*", - "php": ">=8.0", - "utopia-php/servers": "0.1.*" + "php": ">=8.1", + "utopia-php/compression": "0.1.*", + "utopia-php/telemetry": "0.1.*" }, "require-dev": { - "ext-xdebug": "*", "laravel/pint": "^1.2", "phpbench/phpbench": "^1.2", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25", - "swoole/ide-helper": "4.8.3" + "phpunit/phpunit": "^9.5.25" }, "type": "library", "autoload": { @@ -349,18 +2131,17 @@ "license": [ "MIT" ], - "description": "A simple, light and advanced PHP HTTP framework", + "description": "A simple, light and advanced PHP framework", "keywords": [ "framework", - "http", "php", "upf" ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/1.0.2" + "source": "https://github.com/utopia-php/http/tree/0.33.15" }, - "time": "2024-09-10T09:04:19+00:00" + "time": "2024-12-10T13:07:04+00:00" }, { "name": "utopia-php/mongo", @@ -423,57 +2204,54 @@ "time": "2023-09-01T17:25:28+00:00" }, { - "name": "utopia-php/servers", - "version": "0.1.1", + "name": "utopia-php/telemetry", + "version": "0.1.0", "source": { "type": "git", - "url": "https://github.com/utopia-php/servers.git", - "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a" + "url": "https://github.com/utopia-php/telemetry.git", + "reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/fd5c8d32778f265256c1936372a071b944f5ba8a", - "reference": "fd5c8d32778f265256c1936372a071b944f5ba8a", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/d35f2f0632f4ee0be63fb7ace6a94a6adda71a80", + "reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80", "shasum": "" }, "require": { + "ext-opentelemetry": "*", + "ext-protobuf": "*", + "nyholm/psr7": "^1.8", + "open-telemetry/exporter-otlp": "^1.1", + "open-telemetry/sdk": "^1.1", "php": ">=8.0", - "utopia-php/di": "0.1.*" + "symfony/http-client": "^7.1" }, "require-dev": { - "laravel/pint": "^0.2.3", - "phpstan/phpstan": "^1.8", - "phpunit/phpunit": "^9.5.5" + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" }, "type": "library", "autoload": { "psr-4": { - "Utopia\\Servers\\": "src/Servers" + "Utopia\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Team Appwrite", - "email": "team@appwrite.io" - } - ], - "description": "A base library for building Utopia style servers.", "keywords": [ "framework", "php", - "servers", - "upf", - "utopia" + "upf" ], "support": { - "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.1.1" + "issues": "https://github.com/utopia-php/telemetry/issues", + "source": "https://github.com/utopia-php/telemetry/tree/0.1.0" }, - "time": "2024-09-06T02:25:56+00:00" + "time": "2024-11-13T10:29:53+00:00" } ], "packages-dev": [ @@ -678,16 +2456,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -726,7 +2504,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -734,7 +2512,7 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", @@ -1323,16 +3101,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -1343,7 +3121,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -1406,7 +3184,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -1422,60 +3200,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "rregeer/phpunit-coverage-check", @@ -2518,73 +4243,6 @@ }, "time": "2024-06-17T05:45:20+00:00" }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", @@ -2637,29 +4295,25 @@ }, { "name": "utopia-php/cli", - "version": "0.19.0", + "version": "0.14.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "f8af1d6087f498bc1f0191750a118d357ded9948" + "reference": "c30ef985a4e739758a0d95eb0706b357b6d8c086" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/f8af1d6087f498bc1f0191750a118d357ded9948", - "reference": "f8af1d6087f498bc1f0191750a118d357ded9948", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/c30ef985a4e739758a0d95eb0706b357b6d8c086", + "reference": "c30ef985a4e739758a0d95eb0706b357b6d8c086", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/di": "0.1.*", - "utopia-php/framework": "1.0.*" + "utopia-php/framework": "0.*.*" }, "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.6", - "swoole/ide-helper": "4.8.8" + "squizlabs/php_codesniffer": "^3.6" }, "type": "library", "autoload": { @@ -2671,6 +4325,12 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + } + ], "description": "A simple CLI library to manage command line applications", "keywords": [ "cli", @@ -2682,9 +4342,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.19.0" + "source": "https://github.com/utopia-php/cli/tree/0.14.0" }, - "time": "2024-09-05T15:46:56+00:00" + "time": "2022-10-09T10:19:07+00:00" } ], "aliases": [], diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index a5cffd444..1ef3518bf 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -6,7 +6,6 @@ use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Timeout as TimeoutException; -use Utopia\Database\Validator\Authorization; abstract class Adapter { @@ -37,35 +36,6 @@ abstract class Adapter */ protected array $metadata = []; - /** - * @var Authorization - */ - protected Authorization $authorization; - - /** - * @param Authorization $authorization - * - * @return $this - */ - public function setAuthorization(Authorization $authorization): self - { - $this->authorization = $authorization; - - return $this; - } - - /** - * @return $this - */ - public function clearTransformations(): self - { - $this->transformations = [ - '*' => [], - ]; - - return $this; - } - /** * @param string $key * @param mixed $value @@ -901,7 +871,7 @@ protected function getAttributeSelections(array $queries): array */ 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 e98ba76a1..73bf4d6a4 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -12,6 +12,7 @@ use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; class MariaDB extends SQL { @@ -730,7 +731,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; - if (!empty($collectionAttribute['array']) && $this->castIndexArray()) { + if(!empty($collectionAttribute['array']) && $this->castIndexArray()) { $attributes[$i] = '(CAST(' . $attr . ' AS char(' . Database::ARRAY_INDEX_LENGTH . ') ARRAY))'; } } @@ -914,7 +915,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->execute(); } } catch (\Throwable $e) { - if ($e instanceof PDOException) { + if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -962,7 +963,7 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); - if (!empty($document->getInternalId())) { + if(!empty($document->getInternalId())) { $attributes['_id'] = $document->getInternalId(); } @@ -1042,7 +1043,7 @@ public function createDocuments(string $collection, array $documents, int $batch } } } catch (\Throwable $e) { - if ($e instanceof PDOException) { + if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1290,7 +1291,7 @@ public function updateDocument(string $collection, string $id, Document $documen } } catch (\Throwable $e) { - if ($e instanceof PDOException) { + if($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1765,7 +1766,7 @@ public function deleteDocuments(string $collection, array $ids): int public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array { $name = $this->filter($collection); - $roles = $this->authorization->getRoles(); + $roles = Authorization::getRoles(); $where = []; $orders = []; @@ -1847,11 +1848,11 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } $conditions = $this->getSQLConditions($queries); - if (!empty($conditions)) { + if(!empty($conditions)) { $where[] = $conditions; } - if ($this->authorization->getStatus()) { + if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles, $forPermission); } @@ -1968,18 +1969,18 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, public function count(string $collection, array $queries = [], ?int $max = null): int { $name = $this->filter($collection); - $roles = $this->authorization->getRoles(); + $roles = Authorization::getRoles(); $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; $queries = array_map(fn ($query) => clone $query, $queries); $conditions = $this->getSQLConditions($queries); - if (!empty($conditions)) { + if(!empty($conditions)) { $where[] = $conditions; } - if ($this->authorization->getStatus()) { + if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -2041,7 +2042,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): int|float { $name = $this->filter($collection); - $roles = $this->authorization->getRoles(); + $roles = Authorization::getRoles(); $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; @@ -2051,7 +2052,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $where[] = $this->getSQLCondition($query); } - if ($this->authorization->getStatus()) { + if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -2192,7 +2193,7 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - if ($this->getSupportForJSONOverlaps() && $query->onArray()) { + if($this->getSupportForJSONOverlaps() && $query->onArray()) { return "JSON_OVERLAPS(`table_main`.{$attribute}, :{$placeholder}_0)"; } @@ -2218,7 +2219,7 @@ protected function getSQLCondition(Query $query): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if ($array === true) { + if($array === true) { return 'JSON'; } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index b88d575cd..1bd7a095b 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -3,6 +3,7 @@ namespace Utopia\Database\Adapter; use Exception; + use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; @@ -301,7 +302,7 @@ public function getSizeOfCollection(string $collection): int } else { throw new DatabaseException('No size found'); } - } catch (Exception $e) { + } catch(Exception $e) { throw new DatabaseException('Failed to get collection size: ' . $e->getMessage()); } } @@ -1020,8 +1021,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } // permissions - if ($this->authorization->getStatus()) { // skip if authorization is disabled - $roles = \implode('|', $this->authorization->getRoles()); + if (Authorization::$status) { + $roles = \implode('|', Authorization::getRoles()); $filters['_permissions']['$in'] = [new Regex("{$forPermission}\(\".*(?:{$roles}).*\"\)", 'i')]; } @@ -1296,8 +1297,8 @@ public function count(string $collection, array $queries = [], ?int $max = null) $filters = $this->buildFilters($queries); // permissions - if ($this->authorization->getStatus()) { // skip if authorization is disabled - $roles = \implode('|', $this->authorization->getRoles()); + if (Authorization::$status) { // skip if authorization is disabled + $roles = \implode('|', Authorization::getRoles()); $filters['_permissions']['$in'] = [new Regex("read\(\".*(?:{$roles}).*\"\)", 'i')]; } @@ -1324,8 +1325,8 @@ public function sum(string $collection, string $attribute, array $queries = [], $filters = $this->buildFilters($queries); // permissions - if ($this->authorization->getStatus()) { // skip if authorization is disabled - $roles = \implode('|', $this->authorization->getRoles()); + if (Authorization::$status) { // skip if authorization is disabled + $roles = \implode('|', Authorization::getRoles()); $filters['_permissions']['$in'] = [new Regex("read\(\".*(?:{$roles}).*\"\)", 'i')]; } @@ -1433,7 +1434,7 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr $queries = Query::groupByType($queries)['filters']; foreach ($queries as $query) { /* @var $query Query */ - if ($query->isNested()) { + if($query->isNested()) { $operator = $this->getQueryOperator($query->getMethod()); $filters[$separator][] = $this->buildFilters($query->getValues(), $operator); } else { @@ -1487,7 +1488,7 @@ protected function buildFilter(Query $query): array } elseif ($operator == '$ne' && \is_array($value)) { $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { - if ($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { + if($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { $filter[$attribute]['$regex'] = new Regex(".*{$this->escapeWildcards($value)}.*", 'i'); } else { $filter[$attribute]['$in'] = $query->getValues(); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 7d1746fa8..fbc1aaa44 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -13,6 +13,7 @@ use Utopia\Database\Exception\Timeout; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; class Postgres extends SQL { @@ -875,7 +876,7 @@ public function createDocument(string $collection, Document $document): Document $queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions); $stmtPermissions = $this->getPDO()->prepare($queryPermissions); - if ($sqlTenant) { + if($sqlTenant) { $stmtPermissions->bindValue(':_tenant', $this->tenant); } } @@ -934,7 +935,7 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); - if ($this->sharedTables) { + if($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -1691,7 +1692,7 @@ public function deleteDocuments(string $collection, array $ids): int public function find(string $collection, array $queries = [], ?int $limit = 25, ?int $offset = null, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ): array { $name = $this->filter($collection); - $roles = $this->authorization->getRoles(); + $roles = Authorization::getRoles(); $where = []; $orders = []; @@ -1767,7 +1768,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } $conditions = $this->getSQLConditions($queries); - if (!empty($conditions)) { + if(!empty($conditions)) { $where[] = $conditions; } @@ -1775,8 +1776,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $where[] = "table_main._tenant = :_tenant"; } - if ($this->authorization->getStatus()) { - $where[] = $this->getSQLPermissionsCondition($name, $roles, $forPermission); + if (Authorization::$status) { + $where[] = $this->getSQLPermissionsCondition($name, $roles); } $sqlWhere = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; @@ -1888,14 +1889,14 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, public function count(string $collection, array $queries = [], ?int $max = null): int { $name = $this->filter($collection); - $roles = $this->authorization->getRoles(); + $roles = Authorization::getRoles(); $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; $queries = array_map(fn ($query) => clone $query, $queries); $conditions = $this->getSQLConditions($queries); - if (!empty($conditions)) { + if(!empty($conditions)) { $where[] = $conditions; } @@ -1903,7 +1904,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $where[] = "table_main._tenant = :_tenant"; } - if ($this->authorization->getStatus()) { + if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -1954,7 +1955,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): int|float { $name = $this->filter($collection); - $roles = $this->authorization->getRoles(); + $roles = Authorization::getRoles(); $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; @@ -1968,7 +1969,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $where[] = "table_main._tenant = :_tenant"; } - if ($this->authorization->getStatus()) { + if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -2135,7 +2136,7 @@ protected function getFulltextValue(string $value): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if ($array === true) { + if($array === true) { return 'JSONB'; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index b06492f3f..567b0f6c1 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -828,14 +828,14 @@ protected function bindConditionValue(mixed $stmt, Query $query): void return; } - if ($query->isNested()) { + if($query->isNested()) { foreach ($query->getValues() as $value) { $this->bindConditionValue($stmt, $value); } return; } - if ($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { + if($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { $placeholder = $this->getSQLPlaceholder($query) . '_0'; $stmt->bindValue($placeholder, json_encode($query->getValues()), PDO::PARAM_STR); return; @@ -1085,7 +1085,7 @@ public function getSQLConditions(array $queries = [], string $separator = 'AND') continue; } - if ($query->isNested()) { + if($query->isNested()) { $conditions[] = $this->getSQLConditions($query->getValues(), $query->getMethod()); } else { $conditions[] = $this->getSQLCondition($query); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 55494fa55..27d7379ef 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -460,7 +460,7 @@ public function createDocument(string $collection, Document $document): Document $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if ($this->sharedTables) { + if($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -534,7 +534,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions = $this->getPDO()->prepare($queryPermissions); - if ($this->sharedTables) { + if($this->sharedTables) { $stmtPermissions->bindValue(':_tenant', $this->tenant); } } @@ -579,7 +579,7 @@ public function updateDocument(string $collection, string $id, Document $documen $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if ($this->sharedTables) { + if($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -713,7 +713,7 @@ public function updateDocument(string $collection, string $id, Document $documen $stmtAddPermissions = $this->getPDO()->prepare($sql); $stmtAddPermissions->bindValue(":_uid", $document->getId()); - if ($this->sharedTables) { + if($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } diff --git a/src/Database/Database.php b/src/Database/Database.php index 89170c484..8957f0072 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -18,7 +18,6 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\PartialStructure; use Utopia\Database\Validator\Permissions; @@ -346,11 +345,6 @@ class Database */ protected array $relationshipDeleteStack = []; - /** - * @var Authorization - */ - private Authorization $authorization; - /** * @param Adapter $adapter * @param Cache $cache @@ -362,8 +356,6 @@ public function __construct(Adapter $adapter, Cache $cache, array $filters = []) $this->cache = $cache; $this->instanceFilters = $filters; - $this->setAuthorization(new Authorization()); - self::addFilter( 'json', /** @@ -435,29 +427,6 @@ function (?string $value) { ); } - /** - * Sets instance of authorization for permission checks - * - * @param Authorization $authorization - * @return self - */ - public function setAuthorization(Authorization $authorization): self - { - $this->adapter->setAuthorization($authorization); - $this->authorization = $authorization; - return $this; - } - - /** - * Get instance of authorization - * - * @return Authorization - */ - public function getAuthorization(): Authorization - { - return $this->authorization; - } - /** * Add listener to events * @@ -2874,6 +2843,7 @@ public function getDocument(string $collection, string $id, array $queries = [], $queries = \array_values($queries); + $validator = new Authorization(self::PERMISSION_READ); $documentSecurity = $collection->getAttribute('documentSecurity', false); /** @@ -2890,12 +2860,10 @@ public function getDocument(string $collection, string $id, array $queries = [], $document = new Document($cache); if ($collection->getId() !== self::METADATA) { - $isValid = $this->authorization->isValid(new Input(self::PERMISSION_READ, [ + if (!$validator->isValid([ ...$collection->getRead(), ...($documentSecurity ? $document->getRead() : []) - ])); - - if (!$isValid) { + ])) { return new Document(); } } @@ -2914,12 +2882,10 @@ public function getDocument(string $collection, string $id, array $queries = [], $document->setAttribute('$collection', $collection->getId()); if ($collection->getId() !== self::METADATA) { - $isValid = $this->authorization->isValid(new Input(self::PERMISSION_READ, [ + if (!$validator->isValid([ ...$collection->getRead(), ...($documentSecurity ? $document->getRead() : []) - ])); - - if (!$isValid) { + ])) { return new Document(); } } @@ -3244,9 +3210,9 @@ public function createDocument(string $collection, Document $document): Document $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->getId() !== self::METADATA) { - $isValid = $this->authorization->isValid(new Input(self::PERMISSION_CREATE, $collection->getCreate())); - if (!$isValid) { - throw new AuthorizationException($this->authorization->getDescription()); + $authorization = new Authorization(self::PERMISSION_CREATE); + if (!$authorization->isValid($collection->getCreate())) { + throw new AuthorizationException($authorization->getDescription()); } } @@ -3715,7 +3681,7 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->withTransaction(function () use ($collection, $id, $document) { $time = DateTime::now(); - $old = $this->authorization->skip(fn () => $this->silent( + $old = Authorization::skip(fn () => $this->silent( fn () => $this->getDocument($collection->getId(), $id, forUpdate: true) )); @@ -3734,6 +3700,8 @@ public function updateDocument(string $collection, string $id, Document $documen return $attribute['type'] === Database::VAR_RELATIONSHIP; }); + $updateValidator = new Authorization(self::PERMISSION_UPDATE); + $readValidator = new Authorization(self::PERMISSION_READ); $shouldUpdate = false; if ($collection->getId() !== self::METADATA) { @@ -3827,19 +3795,20 @@ public function updateDocument(string $collection, string $id, Document $documen } } - $updateValidator = new Input(self::PERMISSION_UPDATE, [ + $updatePermissions = [ ...$collection->getUpdate(), ...($documentSecurity ? $old->getUpdate() : []) - ]); + ]; - $readValidator = new Input(self::PERMISSION_READ, [ + $readPermissions = [ ...$collection->getRead(), ...($documentSecurity ? $old->getRead() : []) - ]); + ]; - if (($shouldUpdate && !$this->authorization->isValid($updateValidator)) - || (!$shouldUpdate && !$this->authorization->isValid($readValidator))) { - throw new AuthorizationException($this->authorization->getDescription()); + if ($shouldUpdate && !$updateValidator->isValid($updatePermissions)) { + throw new AuthorizationException($updateValidator->getDescription()); + } elseif (!$shouldUpdate && !$readValidator->isValid($readPermissions)) { + throw new AuthorizationException($readValidator->getDescription()); } } @@ -4196,7 +4165,7 @@ private function updateDocumentRelationships(Document $collection, Document $old $removedDocuments = \array_diff($oldIds, $newIds); foreach ($removedDocuments as $relation) { - $this->authorization->skip(fn () => $this->skipRelationships(fn () => $this->updateDocument( + Authorization::skip(fn () => $this->skipRelationships(fn () => $this->updateDocument( $relatedCollection->getId(), $relation, new Document([$twoWayKey => null]) @@ -4326,7 +4295,7 @@ private function updateDocumentRelationships(Document $collection, Document $old ]); foreach ($junctions as $junction) { - $this->authorization->skip(fn () => $this->deleteDocument($junction->getCollection(), $junction->getId())); + Authorization::skip(fn () => $this->deleteDocument($junction->getCollection(), $junction->getId())); } } @@ -4419,10 +4388,12 @@ public function increaseDocumentAttribute(string $collection, string $id, string throw new DatabaseException('Value must be numeric and greater than 0'); } + $validator = new Authorization(self::PERMISSION_UPDATE); + /* @var $document Document */ - $document = $this->authorization->skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this + $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this - if ($document->isEmpty()) { + if($document->isEmpty()) { return false; } @@ -4430,12 +4401,11 @@ public function increaseDocumentAttribute(string $collection, string $id, string if ($collection->getId() !== self::METADATA) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $isValid = $this->authorization->isValid(new Input(self::PERMISSION_UPDATE, [ + if (!$validator->isValid([ ...$collection->getUpdate(), ...($documentSecurity ? $document->getUpdate() : []) - ])); - if (!$isValid) { - throw new AuthorizationException($this->authorization->getDescription()); + ])) { + throw new AuthorizationException($validator->getDescription()); } } @@ -4512,10 +4482,13 @@ public function decreaseDocumentAttribute(string $collection, string $id, string if ($value <= 0) { // Can be a float throw new DatabaseException('Value must be numeric and greater than 0'); } + + $validator = new Authorization(self::PERMISSION_UPDATE); + /* @var $document Document */ - $document = $this->authorization->skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this + $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this - if ($document->isEmpty()) { + if($document->isEmpty()) { return false; } @@ -4523,12 +4496,11 @@ public function decreaseDocumentAttribute(string $collection, string $id, string if ($collection->getId() !== self::METADATA) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $isValid = $this->authorization->isValid(new Input(self::PERMISSION_UPDATE, [ + if (!$validator->isValid([ ...$collection->getUpdate(), ...($documentSecurity ? $document->getUpdate() : []) - ])); - if (!$isValid) { - throw new AuthorizationException($this->authorization->getDescription()); + ])) { + throw new AuthorizationException($validator->getDescription()); } } @@ -4606,23 +4578,24 @@ public function deleteDocument(string $collection, string $id): bool $deleted = $this->withTransaction(function () use ($collection, $id, &$document) { /* @var $document Document */ - $document = $this->authorization->skip(fn () => $this->silent( + $document = Authorization::skip(fn () => $this->silent( fn () => $this->getDocument($collection->getId(), $id, forUpdate: true) )); - if ($document->isEmpty()) { + if($document->isEmpty()) { return false; } + $validator = new Authorization(self::PERMISSION_DELETE); + if ($collection->getId() !== self::METADATA) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $isValid = $this->authorization->isValid(new Input(self::PERMISSION_DELETE, [ + if (!$validator->isValid([ ...$collection->getDelete(), ...($documentSecurity ? $document->getDelete() : []) - ])); - if (!$isValid) { - throw new AuthorizationException($this->authorization->getDescription()); + ])) { + throw new AuthorizationException($validator->getDescription()); } } @@ -4783,7 +4756,7 @@ private function deleteRestrict( && $side === Database::RELATION_SIDE_CHILD && !$twoWay ) { - $this->authorization->skip(function () use ($document, $relatedCollection, $twoWayKey) { + Authorization::skip(function () use ($document, $relatedCollection, $twoWayKey) { $related = $this->findOne($relatedCollection->getId(), [ Query::select(['$id']), Query::equal($twoWayKey, [$document->getId()]) @@ -4807,7 +4780,7 @@ private function deleteRestrict( $relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_CHILD ) { - $related = $this->authorization->skip(fn () => $this->findOne($relatedCollection->getId(), [ + $related = Authorization::skip(fn () => $this->findOne($relatedCollection->getId(), [ Query::select(['$id']), Query::equal($twoWayKey, [$document->getId()]) ])); @@ -4843,7 +4816,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection } // Shouldn't need read or update permission to delete - $this->authorization->skip(function () use ($document, $value, $relatedCollection, $twoWay, $twoWayKey, $side) { + Authorization::skip(function () use ($document, $value, $relatedCollection, $twoWay, $twoWayKey, $side) { if (!$twoWay && $side === Database::RELATION_SIDE_CHILD) { $related = $this->findOne($relatedCollection->getId(), [ Query::select(['$id']), @@ -4875,7 +4848,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection break; } foreach ($value as $relation) { - $this->authorization->skip(function () use ($relatedCollection, $twoWayKey, $relation) { + Authorization::skip(function () use ($relatedCollection, $twoWayKey, $relation) { $this->skipRelationships(fn () => $this->updateDocument( $relatedCollection->getId(), $relation->getId(), @@ -4901,7 +4874,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection } foreach ($value as $relation) { - $this->authorization->skip(function () use ($relatedCollection, $twoWayKey, $relation) { + Authorization::skip(function () use ($relatedCollection, $twoWayKey, $relation) { $this->skipRelationships(fn () => $this->updateDocument( $relatedCollection->getId(), $relation->getId(), @@ -5188,12 +5161,12 @@ public function find(string $collection, array $queries = [], string $forPermiss } } + $authorization = new Authorization(self::PERMISSION_READ); $documentSecurity = $collection->getAttribute('documentSecurity', false); - - $skipAuth = $this->authorization->isValid(new Input($forPermission, $collection->getPermissionsByType($forPermission))); + $skipAuth = $authorization->isValid($collection->getPermissionsByType($forPermission))); if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { - throw new AuthorizationException($this->authorization->getDescription()); + throw new AuthorizationException($authorization->getDescription()); } $relationships = \array_filter( @@ -5281,9 +5254,9 @@ public function find(string $collection, array $queries = [], string $forPermiss $forPermission ); - $results = $skipAuth ? $this->authorization->skip($getResults) : $getResults(); + $results = $skipAuth ? Authorization::skip($getResults) : $getResults(); - foreach ($results as &$node) { + foreach ($results as &$node) { if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) { $node = $this->silent(fn () => $this->populateDocumentRelationships($collection, $node, $nestedSelections)); } @@ -5368,13 +5341,16 @@ public function count(string $collection, array $queries = [], ?int $max = null) } } - $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead())); + $authorization = new Authorization(self::PERMISSION_READ); + if ($authorization->isValid($collection->getRead())) { + $skipAuth = true; + } $queries = Query::groupByType($queries)['filters']; $queries = self::convertQueries($collection, $queries); $getCount = fn () => $this->adapter->count($collection->getId(), $queries, $max); - $count = $skipAuth ? $this->authorization->skip($getCount) : $getCount(); + $count = $skipAuth ?? false ? Authorization::skip($getCount) : $getCount(); $this->trigger(self::EVENT_DOCUMENT_COUNT, $count); diff --git a/src/Database/Exception.php b/src/Database/Exception.php index 94099c6ae..64ad3c997 100644 --- a/src/Database/Exception.php +++ b/src/Database/Exception.php @@ -8,7 +8,7 @@ class Exception extends \Exception { public function __construct(string $message, int|string $code = 0, Throwable $previous = null) { - if (\is_string($code)) { + if(\is_string($code)) { if (\is_numeric($code)) { $code = (int) $code; } else { diff --git a/src/Database/Query.php b/src/Database/Query.php index aab17c195..6af553415 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -297,7 +297,7 @@ public function toArray(): array { $array = ['method' => $this->method]; - if (!empty($this->attribute)) { + if(!empty($this->attribute)) { $array['attribute'] = $this->attribute; } @@ -690,7 +690,7 @@ public static function groupByType(array $queries): array */ public function isNested(): bool { - if (in_array($this->getMethod(), self::LOGICAL_TYPES)) { + if(in_array($this->getMethod(), self::LOGICAL_TYPES)) { return true; } diff --git a/src/Database/Validator/Authorization.php b/src/Database/Validator/Authorization.php index 22d0ebc5c..2dd645ba7 100644 --- a/src/Database/Validator/Authorization.php +++ b/src/Database/Validator/Authorization.php @@ -2,35 +2,34 @@ namespace Utopia\Database\Validator; -use Utopia\Database\Validator\Authorization\Input; -use Utopia\Http\Validator; +use Utopia\Validator; class Authorization extends Validator { /** - * @var bool + * @var array */ - protected bool $status = true; + private static array $roles = [ + 'any' => true + ]; /** - * Default value in case we need - * to reset Authorization status - * - * @var bool + * @var string */ - protected bool $statusDefault = true; + protected string $action = ''; /** - * @var array + * @var string */ - private array $roles = [ - 'any' => true - ]; + protected string $message = 'Authorization Error'; /** - * @var string + * @param string $action */ - protected string $message = 'Authorization Error'; + public function __construct(string $action) + { + $this->action = $action; + } /** * Get Description. @@ -44,43 +43,36 @@ public function getDescription(): string return $this->message; } - /* - * Validation + /** + * Is valid. * * Returns true if valid or false if not. - */ - public function isValid(mixed $input): bool // any, CREATE + * + * @param mixed $permissions + * + * @return bool + */ + public function isValid($permissions): bool { - if (!($input instanceof Input)) { - $this->message = 'Invalid input provided'; - return false; - } - - /** - * @var Input $input - */ - - $permissions = $input->getPermissions(); - $action = $input->getAction(); - - if (!$this->status) { + if (!self::$status) { return true; } if (empty($permissions)) { - $this->message = 'No permissions provided for action \''.$action.'\''; + $this->message = 'No permissions provided for action \''.$this->action.'\''; return false; } $permission = '-'; foreach ($permissions as $permission) { - if (\array_key_exists($permission, $this->roles)) { + if (\array_key_exists($permission, self::$roles)) { return true; } } - $this->message = 'Missing "'.$action.'" permission for role "'.$permission.'". Only "'.\json_encode($this->getRoles()).'" scopes are allowed and "'.\json_encode($permissions).'" was given.'; + $this->message = 'Missing "'.$this->action.'" permission for role "'.$permission.'". Only "'.\json_encode(self::getRoles()).'" scopes are allowed and "'.\json_encode($permissions).'" was given.'; + return false; } @@ -88,9 +80,9 @@ public function isValid(mixed $input): bool // any, CREATE * @param string $role * @return void */ - public function addRole(string $role): void // user:meldiron, users, any + public static function setRole(string $role): void { - $this->roles[$role] = true; + self::$roles[$role] = true; } /** @@ -98,25 +90,25 @@ public function addRole(string $role): void // user:meldiron, users, any * * @return void */ - public function removeRole(string $role): void + public static function unsetRole(string $role): void { - unset($this->roles[$role]); + unset(self::$roles[$role]); } /** * @return array */ - public function getRoles(): array + public static function getRoles(): array { - return \array_keys($this->roles); + return \array_keys(self::$roles); } /** * @return void */ - public function cleanRoles(): void + public static function cleanRoles(): void { - $this->roles = []; + self::$roles = []; } /** @@ -124,53 +116,36 @@ public function cleanRoles(): void * * @return bool */ - public function isRole(string $role): bool + public static function isRole(string $role): bool { - return (\array_key_exists($role, $this->roles)); + return (\array_key_exists($role, self::$roles)); } /** - * Change default status. - * This will be used for the - * value set on the $this->reset() method - * @param bool $status - * @return void + * @var bool */ - public function setDefaultStatus(bool $status): void - { - $this->statusDefault = $status; - $this->status = $status; - } + public static bool $status = true; /** - * Change status - * - * @param bool $status - * @return void - */ - public function setStatus(bool $status): void - { - $this->status = $status; - } - - /** - * Get status + * Default value in case we need + * to reset Authorization status * - * @return bool + * @var bool */ - public function getStatus(): bool - { - return $this->status; - } + public static bool $statusDefault = true; /** - * Get default status + * Change default status. + * This will be used for the + * value set on the self::reset() method * - * @return bool + * @param bool $status + * @return void */ - public function getStatusDefault(): bool + public static function setDefaultStatus(bool $status): void { - return $this->statusDefault; + self::$statusDefault = $status; + self::$status = $status; } /** @@ -182,15 +157,15 @@ public function getStatusDefault(): bool * @param callable(): T $callback * @return T */ - public function skip(callable $callback): mixed + public static function skip(callable $callback): mixed { - $initialStatus = $this->status; - $this->disable(); + $initialStatus = self::$status; + self::disable(); try { return $callback(); } finally { - $this->status = $initialStatus; + self::$status = $initialStatus; } } @@ -199,9 +174,9 @@ public function skip(callable $callback): mixed * * @return void */ - public function enable(): void + public static function enable(): void { - $this->status = true; + self::$status = true; } /** @@ -209,9 +184,9 @@ public function enable(): void * * @return void */ - public function disable(): void + public static function disable(): void { - $this->status = false; + self::$status = false; } /** @@ -219,9 +194,9 @@ public function disable(): void * * @return void */ - public function reset(): void + public static function reset(): void { - $this->status = $this->statusDefault; + self::$status = self::$statusDefault; } /** diff --git a/src/Database/Validator/Authorization/Input.php b/src/Database/Validator/Authorization/Input.php deleted file mode 100644 index 4c2387097..000000000 --- a/src/Database/Validator/Authorization/Input.php +++ /dev/null @@ -1,49 +0,0 @@ -permissions = $permissions; - $this->action = $action; - } - - /** - * @param string[] $permissions - */ - public function setPermissions(array $permissions): self - { - $this->permissions = $permissions; - return $this; - } - - public function setAction(string $action): self - { - $this->action = $action; - return $this; - } - - /** - * @return string[] - */ - public function getPermissions(): array - { - return $this->permissions; - } - - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Database/Validator/Datetime.php b/src/Database/Validator/Datetime.php index 1d8c9dfd2..e8ffee5ff 100644 --- a/src/Database/Validator/Datetime.php +++ b/src/Database/Validator/Datetime.php @@ -2,7 +2,7 @@ namespace Utopia\Database\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; class Datetime extends Validator { @@ -33,7 +33,7 @@ class Datetime extends Validator */ public function __construct(bool $requireDateInFuture = false, string $precision = self::PRECISION_ANY, int $offset = 0) { - if ($offset < 0) { + if($offset < 0) { throw new \Exception('Offset must be a positive number.'); } @@ -50,13 +50,13 @@ public function getDescription(): string { $message = 'Value must be valid date'; - if ($this->offset > 0) { + if($this->offset > 0) { $message .= " at least " . $this->offset . " seconds in future"; - } elseif ($this->requireDateInFuture) { + } elseif($this->requireDateInFuture) { $message .= " in future"; } - if ($this->precision !== self::PRECISION_ANY) { + if($this->precision !== self::PRECISION_ANY) { $message .= " with " . $this->precision . " precision"; } @@ -84,9 +84,9 @@ public function isValid($value): bool return false; } - if ($this->offset !== 0) { + if($this->offset !== 0) { $diff = $date->getTimestamp() - $now->getTimestamp(); - if ($diff <= $this->offset) { + if($diff <= $this->offset) { return false; } } @@ -109,12 +109,12 @@ public function isValid($value): bool break; } - foreach ($denyConstants as $constant) { - if (\intval($date->format($constant)) !== 0) { + foreach($denyConstants as $constant) { + if(\intval($date->format($constant)) !== 0) { return false; } } - } catch (\Exception $e) { + } catch(\Exception $e) { return false; } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index d7f1beba4..b24e39f73 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -5,7 +5,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; -use Utopia\Http\Validator; +use Utopia\Validator; class Index extends Validator { @@ -126,30 +126,30 @@ public function checkArrayIndex(Document $index): bool foreach ($attributes as $attributePosition => $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); - if ($attribute->getAttribute('array', false)) { + if($attribute->getAttribute('array', false)) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values - if ($index->getAttribute('type') != Database::INDEX_KEY) { + if($index->getAttribute('type') != Database::INDEX_KEY) { $this->message = '"' . ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } - if (empty($lengths[$attributePosition])) { + if(empty($lengths[$attributePosition])) { $this->message = 'Index length for array not specified'; return false; } $arrayAttributes[] = $attribute->getAttribute('key', ''); - if (count($arrayAttributes) > 1) { + if(count($arrayAttributes) > 1) { $this->message = 'An index may only contain one array attribute'; return false; } $direction = $orders[$attributePosition] ?? ''; - if (!empty($direction)) { + if(!empty($direction)) { $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; return false; } - } elseif ($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + } elseif($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; return false; } @@ -188,7 +188,7 @@ public function checkIndexLength(Document $index): bool break; } - if ($attribute->getAttribute('array', false)) { + if($attribute->getAttribute('array', false)) { $attributeSize = Database::ARRAY_INDEX_LENGTH; $indexLength = Database::ARRAY_INDEX_LENGTH; } diff --git a/src/Database/Validator/Key.php b/src/Database/Validator/Key.php index 74f175f59..ad1c5df4e 100644 --- a/src/Database/Validator/Key.php +++ b/src/Database/Validator/Key.php @@ -2,7 +2,7 @@ namespace Utopia\Database\Validator; -use Utopia\Http\Validator; +use Utopia\Validator; class Key extends Validator { diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index cc5ae3f5a..2e4aac71a 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -4,7 +4,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Query\Base; -use Utopia\Http\Validator; +use Utopia\Validator; class Queries extends Validator { @@ -71,8 +71,8 @@ public function isValid($value): bool } } - if ($query->isNested()) { - if (!self::isValid($query->getValues())) { + if($query->isNested()) { + if(!self::isValid($query->getValues())) { return false; } } diff --git a/src/Database/Validator/Query/Base.php b/src/Database/Validator/Query/Base.php index fa96b5a69..a37fdd65a 100644 --- a/src/Database/Validator/Query/Base.php +++ b/src/Database/Validator/Query/Base.php @@ -2,7 +2,7 @@ namespace Utopia\Database\Validator\Query; -use Utopia\Http\Validator; +use Utopia\Validator; abstract class Base extends Validator { diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 697c87ee9..635fa4732 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -6,10 +6,10 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\FloatValidator; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\Text; +use Utopia\Validator\Boolean; +use Utopia\Validator\FloatValidator; +use Utopia\Validator\Integer; +use Utopia\Validator\Text; class Filter extends Base { @@ -131,29 +131,29 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } } - if ($attributeSchema['type'] === 'relationship') { + if($attributeSchema['type'] === 'relationship') { /** * We can not disable relationship query since we have logic that use it, * so instead we validate against the relation type */ $options = $attributeSchema['options']; - if ($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { + if($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if ($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + if($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if ($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + if($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if ($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + if($options['relationType'] === Database::RELATION_MANY_TO_MANY) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } @@ -161,7 +161,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $array = $attributeSchema['array'] ?? false; - if ( + if( !$array && $method === Query::TYPE_CONTAINS && $attributeSchema['type'] !== Database::VAR_STRING @@ -170,7 +170,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s return false; } - if ( + if( $array && !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) ) { @@ -253,12 +253,12 @@ public function isValid($value): bool case Query::TYPE_AND: $filters = Query::groupByType($value->getValues())['filters']; - if (count($value->getValues()) !== count($filters)) { + if(count($value->getValues()) !== count($filters)) { $this->message = \ucfirst($method) . ' queries can only contain filter queries'; return false; } - if (count($filters) < 2) { + if(count($filters) < 2) { $this->message = \ucfirst($method) . ' queries require at least two queries'; return false; } diff --git a/src/Database/Validator/Query/Limit.php b/src/Database/Validator/Query/Limit.php index affe0bc89..facc266d7 100644 --- a/src/Database/Validator/Query/Limit.php +++ b/src/Database/Validator/Query/Limit.php @@ -3,8 +3,8 @@ namespace Utopia\Database\Validator\Query; use Utopia\Database\Query; -use Utopia\Http\Validator\Numeric; -use Utopia\Http\Validator\Range; +use Utopia\Validator\Numeric; +use Utopia\Validator\Range; class Limit extends Base { diff --git a/src/Database/Validator/Query/Offset.php b/src/Database/Validator/Query/Offset.php index 6fc7c9564..8d59be4d0 100644 --- a/src/Database/Validator/Query/Offset.php +++ b/src/Database/Validator/Query/Offset.php @@ -3,8 +3,8 @@ namespace Utopia\Database\Validator\Query; use Utopia\Database\Query; -use Utopia\Http\Validator\Numeric; -use Utopia\Http\Validator\Range; +use Utopia\Validator\Numeric; +use Utopia\Validator\Range; class Offset extends Base { diff --git a/src/Database/Validator/Roles.php b/src/Database/Validator/Roles.php index 2db3e8cc7..0306118e7 100644 --- a/src/Database/Validator/Roles.php +++ b/src/Database/Validator/Roles.php @@ -3,7 +3,7 @@ namespace Utopia\Database\Validator; use Utopia\Database\Helpers\Role; -use Utopia\Http\Validator; +use Utopia\Validator; class Roles extends Validator { diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 20388a94c..46b1edafd 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -8,12 +8,12 @@ use Utopia\Database\Document; use Utopia\Database\Exception as DatabaseException; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Http\Validator; -use Utopia\Http\Validator\Boolean; -use Utopia\Http\Validator\FloatValidator; -use Utopia\Http\Validator\Integer; -use Utopia\Http\Validator\Range; -use Utopia\Http\Validator\Text; +use Utopia\Validator; +use Utopia\Validator\Boolean; +use Utopia\Validator\FloatValidator; +use Utopia\Validator\Integer; +use Utopia\Validator\Range; +use Utopia\Validator\Text; class Structure extends Validator { @@ -126,7 +126,7 @@ public static function getFormats(): array * Stores a callback and required params to create Validator * * @param string $name - * @param Closure $callback Callback that accepts $params in order and returns \Utopia\Http\Validator + * @param Closure $callback Callback that accepts $params in order and returns \Utopia\Validator * @param string $type Primitive data type for validation */ public static function addFormat(string $name, Closure $callback, string $type): void @@ -311,7 +311,7 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) continue; } - if ($type === Database::VAR_RELATIONSHIP) { + if($type === Database::VAR_RELATIONSHIP) { continue; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index f3c4b7b79..d9dd2ff77 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -28,7 +28,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Index; use Utopia\Database\Validator\Structure; -use Utopia\Http\Validator\Range; +use Utopia\Validator\Range; ini_set('memory_limit', '2048M'); @@ -36,15 +36,10 @@ abstract class Base extends TestCase { protected static string $namespace; - /** - * @var Authorization - */ - protected static ?Authorization $authorization = null; - /** * @return Database */ - abstract protected function getDatabase(): Database; + abstract protected static function getDatabase(): Database; /** * @return string @@ -53,42 +48,38 @@ abstract protected static function getAdapterName(): string; public function setUp(): void { - if (is_null(self::$authorization)) { - self::$authorization = new Authorization(); - } - - self::$authorization->addRole('any'); + Authorization::setRole('any'); } public function tearDown(): void { - self::$authorization->setDefaultStatus(true); + Authorization::setDefaultStatus(true); } protected string $testDatabase = 'utopiaTests'; public function testPing(): void { - $this->assertEquals(true, $this->getDatabase()->ping()); + $this->assertEquals(true, static::getDatabase()->ping()); } public function testCreateExistsDelete(): void { $schemaSupport = $this->getDatabase()->getAdapter()->getSupportForSchemas(); if (!$schemaSupport) { - $this->assertEquals($this->getDatabase(), $this->getDatabase()->setDatabase($this->testDatabase)); - $this->assertEquals(true, $this->getDatabase()->create()); + $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); + $this->assertEquals(true, static::getDatabase()->create()); return; } - if (!$this->getDatabase()->exists($this->testDatabase)) { - $this->assertEquals(true, $this->getDatabase()->create()); + if (!static::getDatabase()->exists($this->testDatabase)) { + $this->assertEquals(true, static::getDatabase()->create()); } - $this->assertEquals(true, $this->getDatabase()->exists($this->testDatabase)); - $this->assertEquals(true, $this->getDatabase()->delete($this->testDatabase)); - $this->assertEquals(false, $this->getDatabase()->exists($this->testDatabase)); - $this->assertEquals($this->getDatabase(), $this->getDatabase()->setDatabase($this->testDatabase)); - $this->assertEquals(true, $this->getDatabase()->create()); + $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(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); + $this->assertEquals(true, static::getDatabase()->create()); } public function testUpdateDeleteCollectionNotFound(): void @@ -110,163 +101,163 @@ public function testUpdateDeleteCollectionNotFound(): void public function testDeleteRelatedCollection(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createCollection('c2'); + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createCollection('c2'); // ONE_TO_ONE - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_ONE, ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c1')); - $collection = $this->getDatabase()->getCollection('c2'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c1')); + $collection = static::getDatabase()->getCollection('c2'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_ONE, ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c2')); - $collection = $this->getDatabase()->getCollection('c1'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c2')); + $collection = static::getDatabase()->getCollection('c1'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c2'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_ONE, twoWay: true ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c1')); - $collection = $this->getDatabase()->getCollection('c2'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c1')); + $collection = static::getDatabase()->getCollection('c2'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_ONE, twoWay: true ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c2')); - $collection = $this->getDatabase()->getCollection('c1'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c2')); + $collection = static::getDatabase()->getCollection('c1'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); // ONE_TO_MANY - $this->getDatabase()->createCollection('c2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c2'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_MANY, ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c1')); - $collection = $this->getDatabase()->getCollection('c2'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c1')); + $collection = static::getDatabase()->getCollection('c2'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_MANY, ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c2')); - $collection = $this->getDatabase()->getCollection('c1'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c2')); + $collection = static::getDatabase()->getCollection('c1'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c2'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_MANY, twoWay: true ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c1')); - $collection = $this->getDatabase()->getCollection('c2'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c1')); + $collection = static::getDatabase()->getCollection('c2'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_ONE_TO_MANY, twoWay: true ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c2')); - $collection = $this->getDatabase()->getCollection('c1'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c2')); + $collection = static::getDatabase()->getCollection('c1'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); // RELATION_MANY_TO_ONE - $this->getDatabase()->createCollection('c2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c2'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_MANY_TO_ONE, ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c1')); - $collection = $this->getDatabase()->getCollection('c2'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c1')); + $collection = static::getDatabase()->getCollection('c2'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_MANY_TO_ONE, ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c2')); - $collection = $this->getDatabase()->getCollection('c1'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c2')); + $collection = static::getDatabase()->getCollection('c1'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c2'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_MANY_TO_ONE, twoWay: true ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c1')); - $collection = $this->getDatabase()->getCollection('c2'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c1')); + $collection = static::getDatabase()->getCollection('c2'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); - $this->getDatabase()->createCollection('c1'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createCollection('c1'); + static::getDatabase()->createRelationship( collection: 'c1', relatedCollection: 'c2', type: Database::RELATION_MANY_TO_ONE, twoWay: true ); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('c2')); - $collection = $this->getDatabase()->getCollection('c1'); + $this->assertEquals(true, static::getDatabase()->deleteCollection('c2')); + $collection = static::getDatabase()->getCollection('c1'); $this->assertCount(0, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); } @@ -727,27 +718,27 @@ public function testVirtualRelationsAttributes(): void public function testPreserveDatesUpdate(): void { - self::$authorization->disable(); + Authorization::disable(); - $this->getDatabase()->setPreserveDates(true); + static::getDatabase()->setPreserveDates(true); - $this->getDatabase()->createCollection('preserve_update_dates'); + static::getDatabase()->createCollection('preserve_update_dates'); - $this->getDatabase()->createAttribute('preserve_update_dates', 'attr1', Database::VAR_STRING, 10, false); + static::getDatabase()->createAttribute('preserve_update_dates', 'attr1', Database::VAR_STRING, 10, false); - $doc1 = $this->getDatabase()->createDocument('preserve_update_dates', new Document([ + $doc1 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ '$id' => 'doc1', '$permissions' => [], 'attr1' => 'value1', ])); - $doc2 = $this->getDatabase()->createDocument('preserve_update_dates', new Document([ + $doc2 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ '$id' => 'doc2', '$permissions' => [], 'attr1' => 'value2', ])); - $doc3 = $this->getDatabase()->createDocument('preserve_update_dates', new Document([ + $doc3 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ '$id' => 'doc3', '$permissions' => [], 'attr1' => 'value3', @@ -756,8 +747,8 @@ public function testPreserveDatesUpdate(): void $newDate = '2000-01-01T10:00:00.000+00:00'; $doc1->setAttribute('$updatedAt', $newDate); - $this->getDatabase()->updateDocument('preserve_update_dates', 'doc1', $doc1); - $doc1 = $this->getDatabase()->getDocument('preserve_update_dates', 'doc1'); + static::getDatabase()->updateDocument('preserve_update_dates', 'doc1', $doc1); + $doc1 = static::getDatabase()->getDocument('preserve_update_dates', 'doc1'); $this->assertEquals($newDate, $doc1->getAttribute('$updatedAt')); $this->getDatabase()->updateDocuments( @@ -773,38 +764,38 @@ public function testPreserveDatesUpdate(): void ] ); - $doc2 = $this->getDatabase()->getDocument('preserve_update_dates', 'doc2'); - $doc3 = $this->getDatabase()->getDocument('preserve_update_dates', 'doc3'); + $doc2 = static::getDatabase()->getDocument('preserve_update_dates', 'doc2'); + $doc3 = static::getDatabase()->getDocument('preserve_update_dates', 'doc3'); $this->assertEquals($newDate, $doc2->getAttribute('$updatedAt')); $this->assertEquals($newDate, $doc3->getAttribute('$updatedAt')); - $this->getDatabase()->deleteCollection('preserve_update_dates'); + static::getDatabase()->deleteCollection('preserve_update_dates'); - $this->getDatabase()->setPreserveDates(false); + static::getDatabase()->setPreserveDates(false); - self::$authorization->reset(); + Authorization::reset(); } public function testPreserveDatesCreate(): void { - self::$authorization->disable(); + Authorization::disable(); - $this->getDatabase()->setPreserveDates(true); + static::getDatabase()->setPreserveDates(true); - $this->getDatabase()->createCollection('preserve_create_dates'); + static::getDatabase()->createCollection('preserve_create_dates'); - $this->getDatabase()->createAttribute('preserve_create_dates', 'attr1', Database::VAR_STRING, 10, false); + static::getDatabase()->createAttribute('preserve_create_dates', 'attr1', Database::VAR_STRING, 10, false); $date = '2000-01-01T10:00:00.000+00:00'; - $this->getDatabase()->createDocument('preserve_create_dates', new Document([ + static::getDatabase()->createDocument('preserve_create_dates', new Document([ '$id' => 'doc1', '$permissions' => [], 'attr1' => 'value1', '$createdAt' => $date ])); - $this->getDatabase()->createDocuments('preserve_create_dates', [ + static::getDatabase()->createDocuments('preserve_create_dates', [ new Document([ '$id' => 'doc2', '$permissions' => [], @@ -819,18 +810,18 @@ public function testPreserveDatesCreate(): void ]), ], 2); - $doc1 = $this->getDatabase()->getDocument('preserve_create_dates', 'doc1'); - $doc2 = $this->getDatabase()->getDocument('preserve_create_dates', 'doc2'); - $doc3 = $this->getDatabase()->getDocument('preserve_create_dates', 'doc3'); + $doc1 = static::getDatabase()->getDocument('preserve_create_dates', 'doc1'); + $doc2 = static::getDatabase()->getDocument('preserve_create_dates', 'doc2'); + $doc3 = static::getDatabase()->getDocument('preserve_create_dates', 'doc3'); $this->assertEquals($date, $doc1->getAttribute('$createdAt')); $this->assertEquals($date, $doc2->getAttribute('$createdAt')); $this->assertEquals($date, $doc3->getAttribute('$createdAt')); - $this->getDatabase()->deleteCollection('preserve_create_dates'); + static::getDatabase()->deleteCollection('preserve_create_dates'); - $this->getDatabase()->setPreserveDates(false); + static::getDatabase()->setPreserveDates(false); - self::$authorization->reset(); + Authorization::reset(); } /** @@ -880,14 +871,14 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); - $validator = new Index($attributes, $this->getDatabase()->getAdapter()->getMaxIndexLength()); + $validator = new Index($attributes, static::getDatabase()->getAdapter()->getMaxIndexLength()); $errorMessage = 'Index length 701 is larger than the size for title1: 700"'; $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); try { - $this->getDatabase()->createCollection($collection->getId(), $attributes, $indexes, [ + static::getDatabase()->createCollection($collection->getId(), $attributes, $indexes, [ Permission::read(Role::any()), Permission::create(Role::any()), ]); @@ -908,13 +899,13 @@ public function testIndexValidation(): void $collection->setAttribute('indexes', $indexes); - if ($this->getDatabase()->getAdapter()->getMaxIndexLength() > 0) { - $errorMessage = 'Index length is longer than the maximum: ' . $this->getDatabase()->getAdapter()->getMaxIndexLength(); + if (static::getDatabase()->getAdapter()->getMaxIndexLength() > 0) { + $errorMessage = 'Index length is longer than the maximum: ' . static::getDatabase()->getAdapter()->getMaxIndexLength(); $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); try { - $this->getDatabase()->createCollection($collection->getId(), $attributes, $indexes); + static::getDatabase()->createCollection($collection->getId(), $attributes, $indexes); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals($errorMessage, $e->getMessage()); @@ -950,13 +941,13 @@ public function testIndexValidation(): void 'indexes' => $indexes ]); - $validator = new Index($attributes, $this->getDatabase()->getAdapter()->getMaxIndexLength()); + $validator = new Index($attributes, static::getDatabase()->getAdapter()->getMaxIndexLength()); $errorMessage = 'Attribute "integer" cannot be part of a FULLTEXT index, must be of type string'; $this->assertFalse($validator->isValid($indexes[0])); $this->assertEquals($errorMessage, $validator->getDescription()); try { - $this->getDatabase()->createCollection($collection->getId(), $attributes, $indexes); + static::getDatabase()->createCollection($collection->getId(), $attributes, $indexes); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals($errorMessage, $e->getMessage()); @@ -965,9 +956,9 @@ public function testIndexValidation(): void public function testCreatedAtUpdatedAt(): void { - $this->assertInstanceOf('Utopia\Database\Document', $this->getDatabase()->createCollection('created_at')); - $this->getDatabase()->createAttribute('created_at', 'title', Database::VAR_STRING, 100, false); - $document = $this->getDatabase()->createDocument('created_at', new Document([ + $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('created_at')); + static::getDatabase()->createAttribute('created_at', 'title', Database::VAR_STRING, 100, false); + $document = static::getDatabase()->createDocument('created_at', new Document([ '$id' => ID::custom('uid123'), '$permissions' => [ @@ -994,7 +985,7 @@ public function testQueryTimeout(): void $this->assertEquals(true, static::getDatabase()->createAttribute('global-timeouts', 'longtext', Database::VAR_STRING, 100000000, true)); for ($i = 0 ; $i <= 20 ; $i++) { - $this->getDatabase()->createDocument('global-timeouts', new Document([ + static::getDatabase()->createDocument('global-timeouts', new Document([ 'longtext' => file_get_contents(__DIR__ . '/../../resources/longtext.txt'), '$permissions' => [ Permission::read(Role::any()), @@ -1007,11 +998,11 @@ public function testQueryTimeout(): void static::getDatabase()->setTimeout(1); try { - $this->getDatabase()->find('global-timeouts', [ + static::getDatabase()->find('global-timeouts', [ Query::notEqual('longtext', 'appwrite'), ]); $this->fail('Failed to throw exception'); - } catch (TimeoutException $ex) { + } catch(TimeoutException $ex) { static::getDatabase()->clearTimeout(); static::getDatabase()->deleteCollection('global-timeouts'); } @@ -1022,34 +1013,34 @@ public function testQueryTimeout(): void */ public function testCreateListExistsDeleteCollection(): void { - $this->assertInstanceOf('Utopia\Database\Document', $this->getDatabase()->createCollection('actors', permissions: [ + $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('actors', permissions: [ Permission::create(Role::any()), Permission::read(Role::any()), ])); - $this->assertCount(1, $this->getDatabase()->listCollections()); - $this->assertEquals(true, $this->getDatabase()->exists($this->testDatabase, 'actors')); + $this->assertCount(1, static::getDatabase()->listCollections()); + $this->assertEquals(true, static::getDatabase()->exists($this->testDatabase, 'actors')); // Collection names should not be unique - $this->assertInstanceOf('Utopia\Database\Document', $this->getDatabase()->createCollection('actors2', permissions: [ + $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('actors2', permissions: [ Permission::create(Role::any()), Permission::read(Role::any()), ])); - $this->assertCount(2, $this->getDatabase()->listCollections()); - $this->assertEquals(true, $this->getDatabase()->exists($this->testDatabase, 'actors2')); - $collection = $this->getDatabase()->getCollection('actors2'); + $this->assertCount(2, static::getDatabase()->listCollections()); + $this->assertEquals(true, static::getDatabase()->exists($this->testDatabase, 'actors2')); + $collection = static::getDatabase()->getCollection('actors2'); $collection->setAttribute('name', 'actors'); // change name to one that exists - $this->assertInstanceOf('Utopia\Database\Document', $this->getDatabase()->updateDocument( + $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->updateDocument( $collection->getCollection(), $collection->getId(), $collection )); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('actors2')); // Delete collection when finished - $this->assertCount(1, $this->getDatabase()->listCollections()); + $this->assertEquals(true, static::getDatabase()->deleteCollection('actors2')); // Delete collection when finished + $this->assertCount(1, static::getDatabase()->listCollections()); - $this->assertEquals(false, $this->getDatabase()->getCollection('actors')->isEmpty()); - $this->assertEquals(true, $this->getDatabase()->deleteCollection('actors')); - $this->assertEquals(true, $this->getDatabase()->getCollection('actors')->isEmpty()); - $this->assertEquals(false, $this->getDatabase()->exists($this->testDatabase, 'actors')); + $this->assertEquals(false, static::getDatabase()->getCollection('actors')->isEmpty()); + $this->assertEquals(true, static::getDatabase()->deleteCollection('actors')); + $this->assertEquals(true, static::getDatabase()->getCollection('actors')->isEmpty()); + $this->assertEquals(false, static::getDatabase()->exists($this->testDatabase, 'actors')); } public function testSizeCollection(): void @@ -1145,24 +1136,24 @@ public function testSizeCollectionOnDisk(): void public function testSizeFullText(): void { // SQLite does not support fulltext indexes - if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + if (!static::getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('fullTextSizeTest'); + static::getDatabase()->createCollection('fullTextSizeTest'); $size1 = $this->getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); - $this->getDatabase()->createAttribute('fullTextSizeTest', 'string1', Database::VAR_STRING, 128, true); - $this->getDatabase()->createAttribute('fullTextSizeTest', 'string2', Database::VAR_STRING, 254, true); - $this->getDatabase()->createAttribute('fullTextSizeTest', 'string3', Database::VAR_STRING, 254, true); - $this->getDatabase()->createIndex('fullTextSizeTest', 'index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128]); + static::getDatabase()->createAttribute('fullTextSizeTest', 'string1', Database::VAR_STRING, 128, true); + static::getDatabase()->createAttribute('fullTextSizeTest', 'string2', Database::VAR_STRING, 254, true); + static::getDatabase()->createAttribute('fullTextSizeTest', 'string3', Database::VAR_STRING, 254, true); + static::getDatabase()->createIndex('fullTextSizeTest', 'index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128]); $loopCount = 10; for ($i = 0; $i < $loopCount; $i++) { - $this->getDatabase()->createDocument('fullTextSizeTest', new Document([ + static::getDatabase()->createDocument('fullTextSizeTest', new Document([ 'string1' => 'string1' . $i, 'string2' => 'string2' . $i, 'string3' => 'string3' . $i, @@ -1173,7 +1164,7 @@ public function testSizeFullText(): void $this->assertGreaterThan($size1, $size2); - $this->getDatabase()->createIndex('fullTextSizeTest', 'fulltext_index', Database::INDEX_FULLTEXT, ['string1']); + static::getDatabase()->createIndex('fullTextSizeTest', 'fulltext_index', Database::INDEX_FULLTEXT, ['string1']); $size3 = $this->getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); @@ -1182,12 +1173,12 @@ public function testSizeFullText(): void public function testPurgeCollectionCache(): void { - $this->getDatabase()->createCollection('redis'); + static::getDatabase()->createCollection('redis'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('redis', 'name', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('redis', 'age', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('redis', 'name', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('redis', 'age', Database::VAR_INTEGER, 0, true)); - $this->getDatabase()->createDocument('redis', new Document([ + static::getDatabase()->createDocument('redis', new Document([ '$id' => 'doc1', 'name' => 'Richard', 'age' => 15, @@ -1196,106 +1187,106 @@ public function testPurgeCollectionCache(): void ] ])); - $document = $this->getDatabase()->getDocument('redis', 'doc1'); + $document = static::getDatabase()->getDocument('redis', 'doc1'); $this->assertEquals('Richard', $document->getAttribute('name')); $this->assertEquals(15, $document->getAttribute('age')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('redis', 'age')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('redis', 'age')); - $document = $this->getDatabase()->getDocument('redis', 'doc1'); + $document = static::getDatabase()->getDocument('redis', 'doc1'); $this->assertEquals('Richard', $document->getAttribute('name')); $this->assertArrayNotHasKey('age', $document); - $this->assertEquals(true, $this->getDatabase()->createAttribute('redis', 'age', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('redis', 'age', Database::VAR_INTEGER, 0, true)); - $document = $this->getDatabase()->getDocument('redis', 'doc1'); + $document = static::getDatabase()->getDocument('redis', 'doc1'); $this->assertEquals('Richard', $document->getAttribute('name')); $this->assertArrayHasKey('age', $document); } public function testCreateDeleteAttribute(): void { - $this->getDatabase()->createCollection('attributes'); + static::getDatabase()->createCollection('attributes'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16382 + 1, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'string3', Database::VAR_STRING, 65535 + 1, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'string4', Database::VAR_STRING, 16777215 + 1, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'integer', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'bigint', Database::VAR_INTEGER, 8, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'float', Database::VAR_FLOAT, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'boolean', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16382 + 1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string3', Database::VAR_STRING, 65535 + 1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string4', Database::VAR_STRING, 16777215 + 1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'bigint', Database::VAR_INTEGER, 8, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'float', Database::VAR_FLOAT, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'boolean', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createIndex('attributes', 'string1_index', Database::INDEX_KEY, ['string1'])); - $this->assertEquals(true, $this->getDatabase()->createIndex('attributes', 'string2_index', Database::INDEX_KEY, ['string2'], [255])); - $this->assertEquals(true, $this->getDatabase()->createIndex('attributes', 'multi_index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128])); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'string1_index', Database::INDEX_KEY, ['string1'])); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'string2_index', Database::INDEX_KEY, ['string2'], [255])); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'multi_index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128])); - $collection = $this->getDatabase()->getCollection('attributes'); + $collection = static::getDatabase()->getCollection('attributes'); $this->assertCount(8, $collection->getAttribute('attributes')); $this->assertCount(3, $collection->getAttribute('indexes')); // Array - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'string_list', Database::VAR_STRING, 128, true, null, true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'integer_list', Database::VAR_INTEGER, 0, true, null, true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'float_list', Database::VAR_FLOAT, 0, true, null, true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'boolean_list', Database::VAR_BOOLEAN, 0, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string_list', Database::VAR_STRING, 128, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer_list', Database::VAR_INTEGER, 0, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'float_list', Database::VAR_FLOAT, 0, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'boolean_list', Database::VAR_BOOLEAN, 0, true, null, true, true)); - $collection = $this->getDatabase()->getCollection('attributes'); + $collection = static::getDatabase()->getCollection('attributes'); $this->assertCount(12, $collection->getAttribute('attributes')); // Default values - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'string_default', Database::VAR_STRING, 256, false, 'test')); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'integer_default', Database::VAR_INTEGER, 0, false, 1)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'float_default', Database::VAR_FLOAT, 0, false, 1.5)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'boolean_default', Database::VAR_BOOLEAN, 0, false, false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'datetime_default', Database::VAR_DATETIME, 0, false, '2000-06-12T14:12:55.000+00:00', true, false, null, [], ['datetime'])); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string_default', Database::VAR_STRING, 256, false, 'test')); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer_default', Database::VAR_INTEGER, 0, false, 1)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'float_default', Database::VAR_FLOAT, 0, false, 1.5)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'boolean_default', Database::VAR_BOOLEAN, 0, false, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'datetime_default', Database::VAR_DATETIME, 0, false, '2000-06-12T14:12:55.000+00:00', true, false, null, [], ['datetime'])); - $collection = $this->getDatabase()->getCollection('attributes'); + $collection = static::getDatabase()->getCollection('attributes'); $this->assertCount(17, $collection->getAttribute('attributes')); // Delete - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'string1')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'string2')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'string3')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'string4')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'integer')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'bigint')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'float')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'boolean')); - - $collection = $this->getDatabase()->getCollection('attributes'); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string1')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string2')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string3')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string4')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'integer')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'bigint')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'float')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'boolean')); + + $collection = static::getDatabase()->getCollection('attributes'); $this->assertCount(9, $collection->getAttribute('attributes')); $this->assertCount(0, $collection->getAttribute('indexes')); // Delete Array - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'string_list')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'integer_list')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'float_list')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'boolean_list')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string_list')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'integer_list')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'float_list')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'boolean_list')); - $collection = $this->getDatabase()->getCollection('attributes'); + $collection = static::getDatabase()->getCollection('attributes'); $this->assertCount(5, $collection->getAttribute('attributes')); // Delete default - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'string_default')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'integer_default')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'float_default')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'boolean_default')); - $this->assertEquals(true, $this->getDatabase()->deleteAttribute('attributes', 'datetime_default')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'string_default')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'integer_default')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'float_default')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'boolean_default')); + $this->assertEquals(true, static::getDatabase()->deleteAttribute('attributes', 'datetime_default')); - $collection = $this->getDatabase()->getCollection('attributes'); + $collection = static::getDatabase()->getCollection('attributes'); $this->assertCount(0, $collection->getAttribute('attributes')); // Test for custom chars in ID - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'as_5dasdasdas', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'as5dasdasdas_', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', '.as5dasdasdas', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', '-as5dasdasdas', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'as-5dasdasdas', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'as5dasdasdas-', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'socialAccountForYoutubeSubscribersss', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', '5f058a89258075f058a89258075f058t9214', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'as_5dasdasdas', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'as5dasdasdas_', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', '.as5dasdasdas', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', '-as5dasdasdas', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'as-5dasdasdas', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'as5dasdasdas-', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'socialAccountForYoutubeSubscribersss', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', '5f058a89258075f058a89258075f058t9214', Database::VAR_BOOLEAN, 0, true)); // Test non-shared tables duplicates throw duplicate static::getDatabase()->createAttribute('attributes', 'duplicate', Database::VAR_STRING, 128, true); @@ -1338,7 +1329,7 @@ public function invalidDefaultValues(): array public function testInvalidDefaultValues(string $type, mixed $default): void { $this->expectException(\Exception::class); - $this->assertEquals(false, $this->getDatabase()->createAttribute('attributes', 'bad_default', $type, 256, true, $default)); + $this->assertEquals(false, static::getDatabase()->createAttribute('attributes', 'bad_default', $type, 256, true, $default)); } /** @@ -1346,18 +1337,19 @@ public function testInvalidDefaultValues(string $type, mixed $default): void */ public function testAttributeCaseInsensitivity(): void { + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'caseSensitive', Database::VAR_STRING, 128, true)); $this->expectException(DuplicateException::class); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributes', 'CaseSensitive', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'CaseSensitive', Database::VAR_STRING, 128, true)); } public function testAttributeKeyWithSymbols(): void { - $this->getDatabase()->createCollection('attributesWithKeys'); + static::getDatabase()->createCollection('attributesWithKeys'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('attributesWithKeys', 'key_with.sym$bols', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributesWithKeys', 'key_with.sym$bols', Database::VAR_STRING, 128, true)); - $document = $this->getDatabase()->createDocument('attributesWithKeys', new Document([ + $document = static::getDatabase()->createDocument('attributesWithKeys', new Document([ 'key_with.sym$bols' => 'value', '$permissions' => [ Permission::read(Role::any()), @@ -1366,7 +1358,7 @@ public function testAttributeKeyWithSymbols(): void $this->assertEquals('value', $document->getAttribute('key_with.sym$bols')); - $document = $this->getDatabase()->getDocument('attributesWithKeys', $document->getId()); + $document = static::getDatabase()->getDocument('attributesWithKeys', $document->getId()); $this->assertEquals('value', $document->getAttribute('key_with.sym$bols')); } @@ -1374,7 +1366,7 @@ public function testAttributeKeyWithSymbols(): void public function testCollectionNotFound(): void { try { - $this->getDatabase()->find('not_exist', []); + static::getDatabase()->find('not_exist', []); $this->fail('Failed to throw Exception'); } catch (Exception $e) { $this->assertEquals('Collection not found', $e->getMessage()); @@ -1383,9 +1375,9 @@ public function testCollectionNotFound(): void public function testAttributeNamesWithDots(): void { - $this->getDatabase()->createCollection('dots.parent'); + static::getDatabase()->createCollection('dots.parent'); - $this->assertTrue($this->getDatabase()->createAttribute( + $this->assertTrue(static::getDatabase()->createAttribute( collection: 'dots.parent', id: 'dots.name', type: Database::VAR_STRING, @@ -1393,14 +1385,14 @@ public function testAttributeNamesWithDots(): void required: false )); - $document = $this->getDatabase()->find('dots.parent', [ + $document = static::getDatabase()->find('dots.parent', [ Query::select(['dots.name']), ]); $this->assertEmpty($document); - $this->getDatabase()->createCollection('dots'); + static::getDatabase()->createCollection('dots'); - $this->assertTrue($this->getDatabase()->createAttribute( + $this->assertTrue(static::getDatabase()->createAttribute( collection: 'dots', id: 'name', type: Database::VAR_STRING, @@ -1408,13 +1400,13 @@ public function testAttributeNamesWithDots(): void required: false )); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'dots.parent', relatedCollection: 'dots', type: Database::RELATION_ONE_TO_ONE ); - $this->getDatabase()->createDocument('dots.parent', new Document([ + static::getDatabase()->createDocument('dots.parent', new Document([ '$id' => ID::custom('father'), 'dots.name' => 'Bill clinton', '$permissions' => [ @@ -1434,7 +1426,7 @@ public function testAttributeNamesWithDots(): void ] ])); - $documents = $this->getDatabase()->find('dots.parent', [ + $documents = static::getDatabase()->find('dots.parent', [ Query::select(['*']), ]); @@ -1446,9 +1438,9 @@ public function testAttributeNamesWithDots(): void */ public function testIndexCaseInsensitivity(): void { - $this->assertEquals(true, $this->getDatabase()->createIndex('attributes', 'key_caseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_caseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); $this->expectException(DuplicateException::class); - $this->assertEquals(true, $this->getDatabase()->createIndex('attributes', 'key_CaseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_CaseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); } /** @@ -1458,7 +1450,7 @@ public function testIndexCaseInsensitivity(): void */ public function testCleanupAttributeTests(): void { - $this->getDatabase()->deleteCollection('attributes'); + static::getDatabase()->deleteCollection('attributes'); $this->assertEquals(1, 1); } @@ -1469,39 +1461,39 @@ public function testCleanupAttributeTests(): void public function testUnknownFormat(): void { $this->expectException(\Exception::class); - $this->assertEquals(false, $this->getDatabase()->createAttribute('attributes', 'bad_format', Database::VAR_STRING, 256, true, null, true, false, 'url')); + $this->assertEquals(false, static::getDatabase()->createAttribute('attributes', 'bad_format', Database::VAR_STRING, 256, true, null, true, false, 'url')); } public function testCreateDeleteIndex(): void { - $this->getDatabase()->createCollection('indexes'); + static::getDatabase()->createCollection('indexes'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('indexes', 'string', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('indexes', 'order', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('indexes', 'integer', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('indexes', 'float', Database::VAR_FLOAT, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('indexes', 'boolean', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'string', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'order', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'integer', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'float', Database::VAR_FLOAT, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'boolean', Database::VAR_BOOLEAN, 0, true)); // Indexes - $this->assertEquals(true, $this->getDatabase()->createIndex('indexes', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC])); - $this->assertEquals(true, $this->getDatabase()->createIndex('indexes', 'index2', Database::INDEX_KEY, ['float', 'integer'], [], [Database::ORDER_ASC, Database::ORDER_DESC])); - $this->assertEquals(true, $this->getDatabase()->createIndex('indexes', 'index3', Database::INDEX_KEY, ['integer', 'boolean'], [], [Database::ORDER_ASC, Database::ORDER_DESC, Database::ORDER_DESC])); - $this->assertEquals(true, $this->getDatabase()->createIndex('indexes', 'index4', Database::INDEX_UNIQUE, ['string'], [128], [Database::ORDER_ASC])); - $this->assertEquals(true, $this->getDatabase()->createIndex('indexes', 'index5', Database::INDEX_UNIQUE, ['$id', 'string'], [128], [Database::ORDER_ASC])); - $this->assertEquals(true, $this->getDatabase()->createIndex('indexes', 'order', Database::INDEX_UNIQUE, ['order'], [128], [Database::ORDER_ASC])); - - $collection = $this->getDatabase()->getCollection('indexes'); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index2', Database::INDEX_KEY, ['float', 'integer'], [], [Database::ORDER_ASC, Database::ORDER_DESC])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index3', Database::INDEX_KEY, ['integer', 'boolean'], [], [Database::ORDER_ASC, Database::ORDER_DESC, Database::ORDER_DESC])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index4', Database::INDEX_UNIQUE, ['string'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index5', Database::INDEX_UNIQUE, ['$id', 'string'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'order', Database::INDEX_UNIQUE, ['order'], [128], [Database::ORDER_ASC])); + + $collection = static::getDatabase()->getCollection('indexes'); $this->assertCount(6, $collection->getAttribute('indexes')); // Delete Indexes - $this->assertEquals(true, $this->getDatabase()->deleteIndex('indexes', 'index1')); - $this->assertEquals(true, $this->getDatabase()->deleteIndex('indexes', 'index2')); - $this->assertEquals(true, $this->getDatabase()->deleteIndex('indexes', 'index3')); - $this->assertEquals(true, $this->getDatabase()->deleteIndex('indexes', 'index4')); - $this->assertEquals(true, $this->getDatabase()->deleteIndex('indexes', 'index5')); - $this->assertEquals(true, $this->getDatabase()->deleteIndex('indexes', 'order')); - - $collection = $this->getDatabase()->getCollection('indexes'); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index1')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index2')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index3')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index4')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index5')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'order')); + + $collection = static::getDatabase()->getCollection('indexes'); $this->assertCount(0, $collection->getAttribute('indexes')); // Test non-shared tables duplicates throw duplicate @@ -1572,7 +1564,7 @@ public function testCreateCollectionWithSchema(): void ]), ]; - $collection = $this->getDatabase()->createCollection('withSchema', $attributes, $indexes); + $collection = static::getDatabase()->createCollection('withSchema', $attributes, $indexes); $this->assertEquals(false, $collection->isEmpty()); $this->assertEquals('withSchema', $collection->getId()); @@ -1595,10 +1587,10 @@ public function testCreateCollectionWithSchema(): void $this->assertEquals('index3', $collection->getAttribute('indexes')[2]['$id']); $this->assertEquals(Database::INDEX_KEY, $collection->getAttribute('indexes')[2]['type']); - $this->getDatabase()->deleteCollection('withSchema'); + static::getDatabase()->deleteCollection('withSchema'); // Test collection with dash (+attribute +index) - $collection2 = $this->getDatabase()->createCollection('with-dash', [ + $collection2 = static::getDatabase()->createCollection('with-dash', [ new Document([ '$id' => ID::custom('attribute-one'), 'type' => Database::VAR_STRING, @@ -1628,7 +1620,7 @@ public function testCreateCollectionWithSchema(): void $this->assertCount(1, $collection2->getAttribute('indexes')); $this->assertEquals('index-one', $collection2->getAttribute('indexes')[0]['$id']); $this->assertEquals(Database::INDEX_KEY, $collection2->getAttribute('indexes')[0]['type']); - $this->getDatabase()->deleteCollection('with-dash'); + static::getDatabase()->deleteCollection('with-dash'); } public function testCreateCollectionValidator(): void @@ -1727,7 +1719,7 @@ public function testCreateCollectionValidator(): void ]; foreach ($collections as $id) { - $collection = $this->getDatabase()->createCollection($id, $attributes, $indexes); + $collection = static::getDatabase()->createCollection($id, $attributes, $indexes); $this->assertEquals(false, $collection->isEmpty()); $this->assertEquals($id, $collection->getId()); @@ -1754,27 +1746,27 @@ public function testCreateCollectionValidator(): void $this->assertEquals('index.4', $collection->getAttribute('indexes')[3]['$id']); $this->assertEquals(Database::INDEX_KEY, $collection->getAttribute('indexes')[3]['type']); - $this->getDatabase()->deleteCollection($id); + static::getDatabase()->deleteCollection($id); } } public function testCreateDocument(): Document { - $this->getDatabase()->createCollection('documents'); - - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'string', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'integer_signed', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'integer_unsigned', Database::VAR_INTEGER, 4, true, signed: false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'bigint_signed', Database::VAR_INTEGER, 8, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'bigint_unsigned', Database::VAR_INTEGER, 9, true, signed: false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'float_signed', Database::VAR_FLOAT, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'float_unsigned', Database::VAR_FLOAT, 0, true, signed: false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'boolean', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'colors', Database::VAR_STRING, 32, true, null, true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'empty', Database::VAR_STRING, 32, false, null, true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents', 'with-dash', Database::VAR_STRING, 128, false, null)); - - $document = $this->getDatabase()->createDocument('documents', new Document([ + static::getDatabase()->createCollection('documents'); + + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'string', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'integer_signed', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'integer_unsigned', Database::VAR_INTEGER, 4, true, signed: false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'bigint_signed', Database::VAR_INTEGER, 8, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'bigint_unsigned', Database::VAR_INTEGER, 9, true, signed: false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'float_signed', Database::VAR_FLOAT, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'float_unsigned', Database::VAR_FLOAT, 0, true, signed: false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'boolean', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'colors', Database::VAR_STRING, 32, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'empty', Database::VAR_STRING, 32, false, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents', 'with-dash', Database::VAR_STRING, 128, false, null)); + + $document = static::getDatabase()->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user(ID::custom('1'))), @@ -1825,7 +1817,7 @@ public function testCreateDocument(): Document $this->assertEquals('Works', $document->getAttribute('with-dash')); // Test create document with manual internal id - $manualIdDocument = $this->getDatabase()->createDocument('documents', new Document([ + $manualIdDocument = static::getDatabase()->createDocument('documents', new Document([ '$id' => '56000', '$internalId' => '56000', '$permissions' => [ @@ -1878,7 +1870,7 @@ public function testCreateDocument(): Document $this->assertEquals([], $manualIdDocument->getAttribute('empty')); $this->assertEquals('Works', $manualIdDocument->getAttribute('with-dash')); - $manualIdDocument = $this->getDatabase()->getDocument('documents', '56000'); + $manualIdDocument = static::getDatabase()->getDocument('documents', '56000'); $this->assertEquals('56000', $manualIdDocument->getInternalId()); $this->assertNotEmpty(true, $manualIdDocument->getId()); @@ -1917,7 +1909,7 @@ public function testCreateDocument(): Document 'empty' => [], ])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertTrue($e instanceof StructureException); $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); } @@ -1936,7 +1928,7 @@ public function testCreateDocument(): Document 'empty' => [], ])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertTrue($e instanceof StructureException); $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); } @@ -1952,11 +1944,11 @@ public function testCreateDocuments(): array $count = 3; $collection = 'testCreateDocuments'; - $this->getDatabase()->createCollection($collection); + static::getDatabase()->createCollection($collection); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'string', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'integer', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'bigint', Database::VAR_INTEGER, 8, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'string', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'integer', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'bigint', Database::VAR_INTEGER, 8, true)); // Create an array of documents with random attributes. Don't use the createDocument function $documents = []; @@ -1975,7 +1967,7 @@ public function testCreateDocuments(): array ]); } - $documents = $this->getDatabase()->createDocuments($collection, $documents, 3); + $documents = static::getDatabase()->createDocuments($collection, $documents, 3); $this->assertEquals($count, count($documents)); @@ -1994,15 +1986,15 @@ public function testCreateDocuments(): array public function testRespectNulls(): Document { - $this->getDatabase()->createCollection('documents_nulls'); + static::getDatabase()->createCollection('documents_nulls'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents_nulls', 'string', Database::VAR_STRING, 128, false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents_nulls', 'integer', Database::VAR_INTEGER, 0, false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents_nulls', 'bigint', Database::VAR_INTEGER, 8, false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents_nulls', 'float', Database::VAR_FLOAT, 0, false)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('documents_nulls', 'boolean', Database::VAR_BOOLEAN, 0, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents_nulls', 'string', Database::VAR_STRING, 128, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents_nulls', 'integer', Database::VAR_INTEGER, 0, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents_nulls', 'bigint', Database::VAR_INTEGER, 8, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents_nulls', 'float', Database::VAR_FLOAT, 0, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('documents_nulls', 'boolean', Database::VAR_BOOLEAN, 0, false)); - $document = $this->getDatabase()->createDocument('documents_nulls', new Document([ + $document = static::getDatabase()->createDocument('documents_nulls', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -2030,16 +2022,16 @@ public function testRespectNulls(): Document public function testCreateDocumentDefaults(): void { - $this->getDatabase()->createCollection('defaults'); + static::getDatabase()->createCollection('defaults'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('defaults', 'string', Database::VAR_STRING, 128, false, 'default')); - $this->assertEquals(true, $this->getDatabase()->createAttribute('defaults', 'integer', Database::VAR_INTEGER, 0, false, 1)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('defaults', 'float', Database::VAR_FLOAT, 0, false, 1.5)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('defaults', 'boolean', Database::VAR_BOOLEAN, 0, false, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('defaults', 'colors', Database::VAR_STRING, 32, false, ['red', 'green', 'blue'], true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('defaults', 'datetime', Database::VAR_DATETIME, 0, false, '2000-06-12T14:12:55.000+00:00', true, false, null, [], ['datetime'])); + $this->assertEquals(true, static::getDatabase()->createAttribute('defaults', 'string', Database::VAR_STRING, 128, false, 'default')); + $this->assertEquals(true, static::getDatabase()->createAttribute('defaults', 'integer', Database::VAR_INTEGER, 0, false, 1)); + $this->assertEquals(true, static::getDatabase()->createAttribute('defaults', 'float', Database::VAR_FLOAT, 0, false, 1.5)); + $this->assertEquals(true, static::getDatabase()->createAttribute('defaults', 'boolean', Database::VAR_BOOLEAN, 0, false, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('defaults', 'colors', Database::VAR_STRING, 32, false, ['red', 'green', 'blue'], true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('defaults', 'datetime', Database::VAR_DATETIME, 0, false, '2000-06-12T14:12:55.000+00:00', true, false, null, [], ['datetime'])); - $document = $this->getDatabase()->createDocument('defaults', new Document([ + $document = static::getDatabase()->createDocument('defaults', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -2048,7 +2040,7 @@ public function testCreateDocumentDefaults(): void ], ])); - $document2 = $this->getDatabase()->getDocument('defaults', $document->getId()); + $document2 = static::getDatabase()->getDocument('defaults', $document->getId()); $this->assertCount(4, $document2->getPermissions()); $this->assertEquals('read("any")', $document2->getPermissions()[0]); $this->assertEquals('create("any")', $document2->getPermissions()[1]); @@ -2070,7 +2062,7 @@ public function testCreateDocumentDefaults(): void $this->assertEquals('2000-06-12T14:12:55.000+00:00', $document->getAttribute('datetime')); // cleanup collection - $this->getDatabase()->deleteCollection('defaults'); + static::getDatabase()->deleteCollection('defaults'); } /** @@ -2079,14 +2071,14 @@ public function testCreateDocumentDefaults(): void public function testIncreaseDecrease(): Document { $collection = 'increase_decrease'; - $this->getDatabase()->createCollection($collection); + static::getDatabase()->createCollection($collection); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'increase', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'decrease', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'increase_text', Database::VAR_STRING, 255, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'increase_float', Database::VAR_FLOAT, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'increase', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'decrease', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'increase_text', Database::VAR_STRING, 255, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'increase_float', Database::VAR_FLOAT, 0, true)); - $document = $this->getDatabase()->createDocument($collection, new Document([ + $document = static::getDatabase()->createDocument($collection, new Document([ 'increase' => 100, 'decrease' => 100, 'increase_float' => 100, @@ -2103,20 +2095,20 @@ public function testIncreaseDecrease(): Document $this->assertEquals(true, static::getDatabase()->increaseDocumentAttribute($collection, $document->getId(), 'increase', 1, 101)); - $document = $this->getDatabase()->getDocument($collection, $document->getId()); + $document = static::getDatabase()->getDocument($collection, $document->getId()); $this->assertEquals(101, $document->getAttribute('increase')); $this->assertNotEquals($updatedAt, $document->getUpdatedAt()); - $this->assertEquals(true, $this->getDatabase()->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98)); - $document = $this->getDatabase()->getDocument($collection, $document->getId()); + $this->assertEquals(true, static::getDatabase()->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98)); + $document = static::getDatabase()->getDocument($collection, $document->getId()); $this->assertEquals(99, $document->getAttribute('decrease')); - $this->assertEquals(true, $this->getDatabase()->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110)); - $document = $this->getDatabase()->getDocument($collection, $document->getId()); + $this->assertEquals(true, static::getDatabase()->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110)); + $document = static::getDatabase()->getDocument($collection, $document->getId()); $this->assertEquals(105.5, $document->getAttribute('increase_float')); - $this->assertEquals(true, $this->getDatabase()->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100)); - $document = $this->getDatabase()->getDocument($collection, $document->getId()); + $this->assertEquals(true, static::getDatabase()->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100)); + $document = static::getDatabase()->getDocument($collection, $document->getId()); $this->assertEquals(104.4, $document->getAttribute('increase_float')); return $document; @@ -2128,7 +2120,7 @@ public function testIncreaseDecrease(): Document public function testIncreaseLimitMax(Document $document): void { $this->expectException(Exception::class); - $this->assertEquals(true, $this->getDatabase()->increaseDocumentAttribute('increase_decrease', $document->getId(), 'increase', 10.5, 102.4)); + $this->assertEquals(true, static::getDatabase()->increaseDocumentAttribute('increase_decrease', $document->getId(), 'increase', 10.5, 102.4)); } /** @@ -2137,7 +2129,7 @@ public function testIncreaseLimitMax(Document $document): void public function testDecreaseLimitMin(Document $document): void { $this->expectException(Exception::class); - $this->assertEquals(false, $this->getDatabase()->decreaseDocumentAttribute('increase_decrease', $document->getId(), 'decrease', 10, 99)); + $this->assertEquals(false, static::getDatabase()->decreaseDocumentAttribute('increase_decrease', $document->getId(), 'decrease', 10, 99)); } /** @@ -2146,7 +2138,7 @@ public function testDecreaseLimitMin(Document $document): void public function testIncreaseTextAttribute(Document $document): void { $this->expectException(Exception::class); - $this->assertEquals(false, $this->getDatabase()->increaseDocumentAttribute('increase_decrease', $document->getId(), 'increase_text')); + $this->assertEquals(false, static::getDatabase()->increaseDocumentAttribute('increase_decrease', $document->getId(), 'increase_text')); } /** @@ -2154,7 +2146,7 @@ public function testIncreaseTextAttribute(Document $document): void */ public function testGetDocument(Document $document): Document { - $document = $this->getDatabase()->getDocument('documents', $document->getId()); + $document = static::getDatabase()->getDocument('documents', $document->getId()); $this->assertNotEmpty(true, $document->getId()); $this->assertIsString($document->getAttribute('string')); @@ -2181,7 +2173,7 @@ public function testGetDocumentSelect(Document $document): Document { $documentId = $document->getId(); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed']), ]); @@ -2202,7 +2194,7 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$permissions', $document); $this->assertArrayNotHasKey('$collection', $document); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed', '$id']), ]); @@ -2213,7 +2205,7 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$permissions', $document); $this->assertArrayNotHasKey('$collection', $document); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed', '$permissions']), ]); @@ -2224,7 +2216,7 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayHasKey('$permissions', $document); $this->assertArrayNotHasKey('$collection', $document); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed', '$internalId']), ]); @@ -2235,7 +2227,7 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$permissions', $document); $this->assertArrayNotHasKey('$collection', $document); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed', '$collection']), ]); @@ -2246,7 +2238,7 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$permissions', $document); $this->assertArrayHasKey('$collection', $document); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed', '$createdAt']), ]); @@ -2257,7 +2249,7 @@ public function testGetDocumentSelect(Document $document): Document $this->assertArrayNotHasKey('$permissions', $document); $this->assertArrayNotHasKey('$collection', $document); - $document = $this->getDatabase()->getDocument('documents', $documentId, [ + $document = static::getDatabase()->getDocument('documents', $documentId, [ Query::select(['string', 'integer_signed', '$updatedAt']), ]); @@ -2284,7 +2276,7 @@ public function testFulltextIndexWithInteger(): void $this->expectExceptionMessage('Attribute "integer_signed" cannot be part of a FULLTEXT index, must be of type string'); } - $this->getDatabase()->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer_signed']); + static::getDatabase()->createIndex('documents', 'fulltext_integer', Database::INDEX_FULLTEXT, ['string','integer_signed']); } public function testListDocumentSearch(): void @@ -2295,8 +2287,8 @@ public function testListDocumentSearch(): void return; } - $this->getDatabase()->createIndex('documents', 'string', Database::INDEX_FULLTEXT, ['string']); - $this->getDatabase()->createDocument('documents', new Document([ + static::getDatabase()->createIndex('documents', 'string', Database::INDEX_FULLTEXT, ['string']); + static::getDatabase()->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -2318,7 +2310,7 @@ public function testListDocumentSearch(): void /** * Allow reserved keywords for search */ - $documents = $this->getDatabase()->find('documents', [ + $documents = static::getDatabase()->find('documents', [ Query::search('string', '*test+alias@email-provider.com'), ]); @@ -2327,7 +2319,7 @@ public function testListDocumentSearch(): void public function testEmptyTenant(): void { - if (static::getDatabase()->getAdapter()->getSharedTables()) { + if(static::getDatabase()->getAdapter()->getSharedTables()) { $this->expectNotToPerformAssertions(); return; } @@ -2358,17 +2350,17 @@ public function testEmptySearch(): void return; } - $documents = $this->getDatabase()->find('documents', [ + $documents = static::getDatabase()->find('documents', [ Query::search('string', ''), ]); $this->assertEquals(0, count($documents)); - $documents = $this->getDatabase()->find('documents', [ + $documents = static::getDatabase()->find('documents', [ Query::search('string', '*'), ]); $this->assertEquals(0, count($documents)); - $documents = $this->getDatabase()->find('documents', [ + $documents = static::getDatabase()->find('documents', [ Query::search('string', '<>'), ]); $this->assertEquals(0, count($documents)); @@ -2453,6 +2445,53 @@ public function testUpdateDocument(Document $document): Document return $document; } + /** + * @depends testCreateDocuments + * @param array $documents + */ + public function testUpdateDocuments(array $documents): void + { + $collection = 'testCreateDocuments'; + + foreach ($documents as $document) { + $document + ->setAttribute('string', 'text📝 updated') + ->setAttribute('integer', 6) + ->setAttribute('$permissions', [ + Permission::read(Role::users()), + Permission::create(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ]); + } + + $documents = static::getDatabase()->updateDocuments( + $collection, + $documents, + \count($documents) + ); + + foreach ($documents as $document) { + $this->assertEquals('text📝 updated', $document->getAttribute('string')); + $this->assertEquals(6, $document->getAttribute('integer')); + } + + $documents = static::getDatabase()->find($collection, [ + Query::limit(\count($documents)) + ]); + + foreach ($documents as $document) { + $this->assertEquals('text📝 updated', $document->getAttribute('string')); + $this->assertEquals(6, $document->getAttribute('integer')); + $this->assertEquals([ + Permission::read(Role::users()), + Permission::create(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], $document->getAttribute('$permissions')); + } + } + /** * @depends testUpdateDocument */ @@ -2471,7 +2510,7 @@ public function testUpdateDocumentConflict(Document $document): void return $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); }); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertTrue($e instanceof ConflictException); $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); } @@ -2534,9 +2573,9 @@ public function testDeleteDocument(Document $document): void */ public function testArrayAttribute(): void { - self::$authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); - $database = $this->getDatabase(); + $database = static::getDatabase(); $collection = 'json'; $permissions = [Permission::read(Role::any())]; @@ -2612,7 +2651,7 @@ public function testArrayAttribute(): void try { $database->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); } @@ -2632,7 +2671,7 @@ public function testArrayAttribute(): void 'short' => ['More than 5 size'], ])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "short[\'0\']" has invalid type. Value must be a valid string and no longer than 5 chars', $e->getMessage()); } @@ -2641,7 +2680,7 @@ public function testArrayAttribute(): void 'names' => ['Joe', 100], ])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "names[\'1\']" has invalid type. Value must be a valid string and no longer than 255 chars', $e->getMessage()); } @@ -2650,7 +2689,7 @@ public function testArrayAttribute(): void 'age' => 1.5, ])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); } @@ -2659,7 +2698,7 @@ public function testArrayAttribute(): void 'age' => -100, ])); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); } @@ -2688,7 +2727,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); } else { @@ -2699,7 +2738,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); } @@ -2720,7 +2759,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); } } @@ -2731,7 +2770,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Cannot set a length on "integer" attributes', $e->getMessage()); } @@ -2744,7 +2783,7 @@ public function testArrayAttribute(): void Query::equal('names', ['Joe']), ]); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid query: Cannot query equal on attribute "names" because it is an array.', $e->getMessage()); } @@ -2753,7 +2792,7 @@ public function testArrayAttribute(): void Query::contains('age', [10]) ]); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array or string.', $e->getMessage()); } @@ -2795,23 +2834,23 @@ public function testArrayAttribute(): void */ public function testFind(): array { - self::$authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); - $this->getDatabase()->createCollection('movies', permissions: [ + static::getDatabase()->createCollection('movies', permissions: [ Permission::create(Role::any()), Permission::update(Role::users()) ]); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'name', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'director', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'year', Database::VAR_INTEGER, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'price', Database::VAR_FLOAT, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'active', Database::VAR_BOOLEAN, 0, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'genres', Database::VAR_STRING, 32, true, null, true, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'with-dash', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, $this->getDatabase()->createAttribute('movies', 'nullable', Database::VAR_STRING, 128, false)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'name', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'director', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'year', Database::VAR_INTEGER, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'price', Database::VAR_FLOAT, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'active', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'genres', Database::VAR_STRING, 32, true, null, true, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'with-dash', Database::VAR_STRING, 128, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('movies', 'nullable', Database::VAR_STRING, 128, false)); - $document = $this->getDatabase()->createDocument('movies', new Document([ + $document = static::getDatabase()->createDocument('movies', new Document([ '$id' => ID::custom('frozen'), '$permissions' => [ Permission::read(Role::any()), @@ -2836,7 +2875,7 @@ public function testFind(): array 'with-dash' => 'Works' ])); - $this->getDatabase()->createDocument('movies', new Document([ + static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -2860,7 +2899,7 @@ public function testFind(): array 'with-dash' => 'Works' ])); - $this->getDatabase()->createDocument('movies', new Document([ + static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -2884,7 +2923,7 @@ public function testFind(): array 'with-dash' => 'Works2' ])); - $this->getDatabase()->createDocument('movies', new Document([ + static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -2908,7 +2947,7 @@ public function testFind(): array 'with-dash' => 'Works2' ])); - $this->getDatabase()->createDocument('movies', new Document([ + static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -2932,7 +2971,7 @@ public function testFind(): array 'with-dash' => 'Works3' ])); - $this->getDatabase()->createDocument('movies', new Document([ + static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::user('x')), Permission::create(Role::any()), @@ -2962,7 +3001,7 @@ public function testFind(): array public function testFindBasicChecks(): void { - $documents = $this->getDatabase()->find('movies'); + $documents = static::getDatabase()->find('movies'); $movieDocuments = $documents; $this->assertEquals(5, count($documents)); @@ -2995,13 +3034,13 @@ public function testFindBasicChecks(): void /** * Check $id: Notice, this orders ID names alphabetically, not by internal numeric ID */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('$id'), ]); $this->assertEquals($lastDocumentId, $documents[0]->getId()); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderAsc('$id'), @@ -3011,13 +3050,13 @@ public function testFindBasicChecks(): void /** * Check internal numeric ID sorting */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc(''), ]); $this->assertEquals($movieDocuments[\count($movieDocuments) - 1]->getId(), $documents[0]->getId()); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderAsc(''), @@ -3030,8 +3069,8 @@ public function testFindCheckPermissions(): void /** * Check Permissions */ - self::$authorization->addRole('user:x'); - $documents = $this->getDatabase()->find('movies'); + Authorization::setRole('user:x'); + $documents = static::getDatabase()->find('movies'); $this->assertEquals(6, count($documents)); } @@ -3041,13 +3080,13 @@ public function testFindCheckInteger(): void /** * Query with dash attribute */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('with-dash', ['Works']), ]); $this->assertEquals(2, count($documents)); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('with-dash', ['Works2', 'Works3']), ]); @@ -3056,7 +3095,7 @@ public function testFindCheckInteger(): void /** * Check an Integer condition */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('year', [2019]), ]); @@ -3070,7 +3109,7 @@ public function testFindBoolean(): void /** * Boolean condition */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('active', [true]), ]); @@ -3082,7 +3121,7 @@ public function testFindStringQueryEqual(): void /** * String condition */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('director', ['TBD']), ]); @@ -3100,7 +3139,7 @@ public function testFindNotEqual(): void /** * Not Equal query */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::notEqual('director', 'TBD'), ]); @@ -3122,22 +3161,22 @@ public function testFindNotEqual(): void public function testFindBetween(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::between('price', 25.94, 25.99), ]); $this->assertEquals(2, count($documents)); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::between('price', 30, 35), ]); $this->assertEquals(0, count($documents)); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::between('$createdAt', '1975-12-06', '2050-12-06'), ]); $this->assertEquals(6, count($documents)); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::between('$updatedAt', '1975-12-06T07:08:49.733+02:00', '2050-02-05T10:15:21.825+00:00'), ]); $this->assertEquals(6, count($documents)); @@ -3148,7 +3187,7 @@ public function testFindFloat(): void /** * Float condition */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::lessThan('price', 26.00), Query::greaterThan('price', 25.98), ]); @@ -3163,7 +3202,7 @@ public function testFindContains(): void return; } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::contains('genres', ['comics']) ]); @@ -3172,24 +3211,24 @@ public function testFindContains(): void /** * Array contains OR condition */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::contains('genres', ['comics', 'kids']), ]); $this->assertEquals(4, count($documents)); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::contains('genres', ['non-existent']), ]); $this->assertEquals(0, count($documents)); try { - $this->getDatabase()->find('movies', [ + static::getDatabase()->find('movies', [ Query::contains('price', [10.5]), ]); $this->fail('Failed to throw exception'); - } catch (Throwable $e) { + } catch(Throwable $e) { $this->assertEquals('Invalid query: Cannot query contains on attribute "price" because it is not an array or string.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } @@ -3201,10 +3240,10 @@ public function testFindFulltext(): void * Fulltext search */ if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { - $success = $this->getDatabase()->createIndex('movies', 'name', Database::INDEX_FULLTEXT, ['name']); + $success = static::getDatabase()->createIndex('movies', 'name', Database::INDEX_FULLTEXT, ['name']); $this->assertEquals(true, $success); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::search('name', 'captain'), ]); @@ -3218,7 +3257,7 @@ public function testFindFulltext(): void // TODO: I think this needs a changes? how do we distinguish between regular full text and wildcard? if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::search('name', 'cap'), ]); @@ -3231,62 +3270,62 @@ public function testFindFulltext(): void public function testFindFulltextSpecialChars(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + if (!static::getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->expectNotToPerformAssertions(); return; } $collection = 'full_text'; - $this->getDatabase()->createCollection($collection, permissions: [ + static::getDatabase()->createCollection($collection, permissions: [ Permission::create(Role::any()), Permission::update(Role::users()) ]); - $this->assertTrue($this->getDatabase()->createAttribute($collection, 'ft', Database::VAR_STRING, 128, true)); - $this->assertTrue($this->getDatabase()->createIndex($collection, 'ft-index', Database::INDEX_FULLTEXT, ['ft'])); + $this->assertTrue(static::getDatabase()->createAttribute($collection, 'ft', Database::VAR_STRING, 128, true)); + $this->assertTrue(static::getDatabase()->createIndex($collection, 'ft-index', Database::INDEX_FULLTEXT, ['ft'])); - $this->getDatabase()->createDocument($collection, new Document([ + static::getDatabase()->createDocument($collection, new Document([ '$permissions' => [Permission::read(Role::any())], 'ft' => 'Alf: chapter_4@nasa.com' ])); - $documents = $this->getDatabase()->find($collection, [ + $documents = static::getDatabase()->find($collection, [ Query::search('ft', 'chapter_4'), ]); $this->assertEquals(1, count($documents)); - $this->getDatabase()->createDocument($collection, new Document([ + static::getDatabase()->createDocument($collection, new Document([ '$permissions' => [Permission::read(Role::any())], 'ft' => 'al@ba.io +-*)(<>~' ])); - $documents = $this->getDatabase()->find($collection, [ + $documents = static::getDatabase()->find($collection, [ Query::search('ft', 'al@ba.io'), // === al ba io* ]); - if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildcardIndex()) { + if (static::getDatabase()->getAdapter()->getSupportForFulltextWildcardIndex()) { $this->assertEquals(0, count($documents)); } else { $this->assertEquals(1, count($documents)); } - $this->getDatabase()->createDocument($collection, new Document([ + static::getDatabase()->createDocument($collection, new Document([ '$permissions' => [Permission::read(Role::any())], 'ft' => 'donald duck' ])); - $this->getDatabase()->createDocument($collection, new Document([ + static::getDatabase()->createDocument($collection, new Document([ '$permissions' => [Permission::read(Role::any())], 'ft' => 'donald trump' ])); - $documents = $this->getDatabase()->find($collection, [ + $documents = static::getDatabase()->find($collection, [ Query::search('ft', 'donald trump'), Query::orderAsc('ft'), ]); $this->assertEquals(2, count($documents)); - $documents = $this->getDatabase()->find($collection, [ + $documents = static::getDatabase()->find($collection, [ Query::search('ft', '"donald trump"'), // Exact match ]); @@ -3298,7 +3337,7 @@ public function testFindMultipleConditions(): void /** * Multiple conditions */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('director', ['TBD']), Query::equal('year', [2026]), ]); @@ -3308,7 +3347,7 @@ public function testFindMultipleConditions(): void /** * Multiple conditions and OR values */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('name', ['Frozen II', 'Captain Marvel']), ]); @@ -3322,7 +3361,7 @@ public function testFindByID(): void /** * $id condition */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('$id', ['frozen']), ]); @@ -3342,7 +3381,7 @@ public function testFindByInternalID(array $data): void /** * Test that internal ID queries are handled correctly */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('$internalId', [$data['$internalId']]), ]); @@ -3355,7 +3394,7 @@ public function testFindByInternalID(array $data): void */ public function testSelectInternalID(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['$internalId', '$id']), Query::orderAsc(''), Query::limit(1), @@ -3366,7 +3405,7 @@ public function testSelectInternalID(): void $this->assertArrayHasKey('$internalId', $document); $this->assertCount(2, $document); - $document = $this->getDatabase()->getDocument('movies', $document->getId(), [ + $document = static::getDatabase()->getDocument('movies', $document->getId(), [ Query::select(['$internalId']), ]); @@ -3379,7 +3418,7 @@ public function testFindOrderBy(): void /** * ORDER BY */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('price'), @@ -3400,11 +3439,11 @@ public function testFindOrderByNatural(): void /** * ORDER BY natural */ - $base = array_reverse($this->getDatabase()->find('movies', [ + $base = array_reverse(static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), ])); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc(''), @@ -3424,7 +3463,7 @@ public function testFindOrderByMultipleAttributes(): void /** * ORDER BY - Multiple attributes */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('price'), @@ -3445,12 +3484,12 @@ public function testFindOrderByCursorAfter(): void /** * ORDER BY - After */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorAfter($movies[1]) @@ -3459,7 +3498,7 @@ public function testFindOrderByCursorAfter(): void $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorAfter($movies[3]) @@ -3468,7 +3507,7 @@ public function testFindOrderByCursorAfter(): void $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorAfter($movies[4]) @@ -3476,7 +3515,7 @@ public function testFindOrderByCursorAfter(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorAfter($movies[5]) @@ -3490,12 +3529,12 @@ public function testFindOrderByCursorBefore(): void /** * ORDER BY - Before */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorBefore($movies[5]) @@ -3504,7 +3543,7 @@ public function testFindOrderByCursorBefore(): void $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorBefore($movies[3]) @@ -3513,7 +3552,7 @@ public function testFindOrderByCursorBefore(): void $this->assertEquals($movies[1]['name'], $documents[0]['name']); $this->assertEquals($movies[2]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorBefore($movies[2]) @@ -3522,7 +3561,7 @@ public function testFindOrderByCursorBefore(): void $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorBefore($movies[1]) @@ -3530,7 +3569,7 @@ public function testFindOrderByCursorBefore(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorBefore($movies[0]) @@ -3543,12 +3582,12 @@ public function testFindOrderByAfterNaturalOrder(): void /** * ORDER BY - After by natural order */ - $movies = array_reverse($this->getDatabase()->find('movies', [ + $movies = array_reverse(static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), ])); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3558,7 +3597,7 @@ public function testFindOrderByAfterNaturalOrder(): void $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3568,7 +3607,7 @@ public function testFindOrderByAfterNaturalOrder(): void $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3577,7 +3616,7 @@ public function testFindOrderByAfterNaturalOrder(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3591,13 +3630,13 @@ public function testFindOrderByBeforeNaturalOrder(): void /** * ORDER BY - Before by natural order */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc(''), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3607,7 +3646,7 @@ public function testFindOrderByBeforeNaturalOrder(): void $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3617,7 +3656,7 @@ public function testFindOrderByBeforeNaturalOrder(): void $this->assertEquals($movies[1]['name'], $documents[0]['name']); $this->assertEquals($movies[2]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3627,7 +3666,7 @@ public function testFindOrderByBeforeNaturalOrder(): void $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3636,7 +3675,7 @@ public function testFindOrderByBeforeNaturalOrder(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc(''), @@ -3650,13 +3689,13 @@ public function testFindOrderBySingleAttributeAfter(): void /** * ORDER BY - Single Attribute After */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('year') ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3667,7 +3706,7 @@ public function testFindOrderBySingleAttributeAfter(): void $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3677,7 +3716,7 @@ public function testFindOrderBySingleAttributeAfter(): void $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3686,7 +3725,7 @@ public function testFindOrderBySingleAttributeAfter(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3700,13 +3739,13 @@ public function testFindOrderBySingleAttributeBefore(): void /** * ORDER BY - Single Attribute Before */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('year') ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3716,7 +3755,7 @@ public function testFindOrderBySingleAttributeBefore(): void $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3726,7 +3765,7 @@ public function testFindOrderBySingleAttributeBefore(): void $this->assertEquals($movies[1]['name'], $documents[0]['name']); $this->assertEquals($movies[2]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3736,7 +3775,7 @@ public function testFindOrderBySingleAttributeBefore(): void $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3745,7 +3784,7 @@ public function testFindOrderBySingleAttributeBefore(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('year'), @@ -3759,14 +3798,14 @@ public function testFindOrderByMultipleAttributeAfter(): void /** * ORDER BY - Multiple Attribute After */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('price'), Query::orderAsc('year') ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3777,7 +3816,7 @@ public function testFindOrderByMultipleAttributeAfter(): void $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3788,7 +3827,7 @@ public function testFindOrderByMultipleAttributeAfter(): void $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3798,7 +3837,7 @@ public function testFindOrderByMultipleAttributeAfter(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3813,14 +3852,14 @@ public function testFindOrderByMultipleAttributeBefore(): void /** * ORDER BY - Multiple Attribute Before */ - $movies = $this->getDatabase()->find('movies', [ + $movies = static::getDatabase()->find('movies', [ Query::limit(25), Query::offset(0), Query::orderDesc('price'), Query::orderAsc('year') ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3832,7 +3871,7 @@ public function testFindOrderByMultipleAttributeBefore(): void $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3843,7 +3882,7 @@ public function testFindOrderByMultipleAttributeBefore(): void $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3854,7 +3893,7 @@ public function testFindOrderByMultipleAttributeBefore(): void $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3864,7 +3903,7 @@ public function testFindOrderByMultipleAttributeBefore(): void $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), @@ -3879,12 +3918,12 @@ public function testFindOrderByAndCursor(): void /** * ORDER BY + CURSOR */ - $documentsTest = $this->getDatabase()->find('movies', [ + $documentsTest = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('price'), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(1), Query::offset(0), Query::orderDesc('price'), @@ -3899,12 +3938,12 @@ public function testFindOrderByIdAndCursor(): void /** * ORDER BY ID + CURSOR */ - $documentsTest = $this->getDatabase()->find('movies', [ + $documentsTest = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('$id'), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(1), Query::offset(0), Query::orderDesc('$id'), @@ -3919,13 +3958,13 @@ public function testFindOrderByCreateDateAndCursor(): void /** * ORDER BY CREATE DATE + CURSOR */ - $documentsTest = $this->getDatabase()->find('movies', [ + $documentsTest = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('$createdAt'), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(1), Query::offset(0), Query::orderDesc('$createdAt'), @@ -3940,12 +3979,12 @@ public function testFindOrderByUpdateDateAndCursor(): void /** * ORDER BY UPDATE DATE + CURSOR */ - $documentsTest = $this->getDatabase()->find('movies', [ + $documentsTest = static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::orderDesc('$updatedAt'), ]); - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(1), Query::offset(0), Query::orderDesc('$updatedAt'), @@ -3960,7 +3999,7 @@ public function testFindLimit(): void /** * Limit */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(4), Query::offset(0), Query::orderAsc('name') @@ -3978,7 +4017,7 @@ public function testFindLimitAndOffset(): void /** * Limit + Offset */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::limit(4), Query::offset(2), Query::orderAsc('name') @@ -3996,7 +4035,7 @@ public function testFindOrQueries(): void /** * Test that OR queries are handled correctly */ - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::equal('director', ['TBD', 'Joe Johnston']), Query::equal('year', [2025]), ]); @@ -4014,7 +4053,7 @@ public function testFindOrderByAfterException(): void ]); $this->expectException(Exception::class); - $this->getDatabase()->find('movies', [ + static::getDatabase()->find('movies', [ Query::limit(2), Query::offset(0), Query::cursorAfter($document) @@ -4028,9 +4067,9 @@ public function testFindEdgeCases(Document $document): void { $collection = 'edgeCases'; - $this->getDatabase()->createCollection($collection); + static::getDatabase()->createCollection($collection); - $this->assertEquals(true, $this->getDatabase()->createAttribute($collection, 'value', Database::VAR_STRING, 256, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute($collection, 'value', Database::VAR_STRING, 256, true)); $values = [ 'NormalString', @@ -4049,7 +4088,7 @@ public function testFindEdgeCases(Document $document): void ]; foreach ($values as $value) { - $this->getDatabase()->createDocument($collection, new Document([ + static::getDatabase()->createDocument($collection, new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), @@ -4063,7 +4102,7 @@ public function testFindEdgeCases(Document $document): void /** * Check Basic */ - $documents = $this->getDatabase()->find($collection); + $documents = static::getDatabase()->find($collection); $this->assertEquals(count($values), count($documents)); $this->assertNotEmpty($documents[0]->getId()); @@ -4077,7 +4116,7 @@ public function testFindEdgeCases(Document $document): void * Check `equals` query */ foreach ($values as $value) { - $documents = $this->getDatabase()->find($collection, [ + $documents = static::getDatabase()->find($collection, [ Query::limit(25), Query::equal('value', [$value]) ]); @@ -4090,13 +4129,13 @@ public function testFindEdgeCases(Document $document): void public function testOrSingleQuery(): void { try { - $this->getDatabase()->find('movies', [ + static::getDatabase()->find('movies', [ Query::or([ Query::equal('active', [true]) ]) ]); $this->fail('Failed to throw exception'); - } catch (Exception $e) { + } catch(Exception $e) { $this->assertEquals('Invalid query: Or queries require at least two queries', $e->getMessage()); } } @@ -4109,8 +4148,8 @@ public function testOrMultipleQueries(): void Query::equal('name', ['Frozen II']) ]) ]; - $this->assertCount(4, $this->getDatabase()->find('movies', $queries)); - $this->assertEquals(4, $this->getDatabase()->count('movies', $queries)); + $this->assertCount(4, static::getDatabase()->find('movies', $queries)); + $this->assertEquals(4, static::getDatabase()->count('movies', $queries)); $queries = [ Query::equal('active', [true]), @@ -4121,8 +4160,8 @@ public function testOrMultipleQueries(): void ]) ]; - $this->assertCount(3, $this->getDatabase()->find('movies', $queries)); - $this->assertEquals(3, $this->getDatabase()->count('movies', $queries)); + $this->assertCount(3, static::getDatabase()->find('movies', $queries)); + $this->assertEquals(3, static::getDatabase()->count('movies', $queries)); } public function testOrNested(): void @@ -4139,24 +4178,24 @@ public function testOrNested(): void ]) ]; - $documents = $this->getDatabase()->find('movies', $queries); + $documents = static::getDatabase()->find('movies', $queries); $this->assertCount(1, $documents); $this->assertArrayNotHasKey('name', $documents[0]); - $count = $this->getDatabase()->count('movies', $queries); + $count = static::getDatabase()->count('movies', $queries); $this->assertEquals(1, $count); } public function testAndSingleQuery(): void { try { - $this->getDatabase()->find('movies', [ + static::getDatabase()->find('movies', [ Query::and([ Query::equal('active', [true]) ]) ]); $this->fail('Failed to throw exception'); - } catch (Exception $e) { + } catch(Exception $e) { $this->assertEquals('Invalid query: And queries require at least two queries', $e->getMessage()); } } @@ -4169,8 +4208,8 @@ public function testAndMultipleQueries(): void Query::equal('name', ['Frozen II']) ]) ]; - $this->assertCount(1, $this->getDatabase()->find('movies', $queries)); - $this->assertEquals(1, $this->getDatabase()->count('movies', $queries)); + $this->assertCount(1, static::getDatabase()->find('movies', $queries)); + $this->assertEquals(1, static::getDatabase()->count('movies', $queries)); } public function testAndNested(): void @@ -4185,16 +4224,16 @@ public function testAndNested(): void ]) ]; - $documents = $this->getDatabase()->find('movies', $queries); + $documents = static::getDatabase()->find('movies', $queries); $this->assertCount(3, $documents); - $count = $this->getDatabase()->count('movies', $queries); + $count = static::getDatabase()->count('movies', $queries); $this->assertEquals(3, $count); } public function testNestedIDQueries(): void { - self::$authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); static::getDatabase()->createCollection('movies_nested_id', permissions: [ Permission::create(Role::any()), @@ -4258,7 +4297,7 @@ public function testNestedIDQueries(): void */ public function testFindOne(): void { - $document = $this->getDatabase()->findOne('movies', [ + $document = static::getDatabase()->findOne('movies', [ Query::offset(2), Query::orderAsc('name') ]); @@ -4266,7 +4305,7 @@ public function testFindOne(): void $this->assertFalse($document->isEmpty()); $this->assertEquals('Frozen', $document->getAttribute('name')); - $document = $this->getDatabase()->findOne('movies', [ + $document = static::getDatabase()->findOne('movies', [ Query::offset(10) ]); $this->assertTrue($document->isEmpty()); @@ -4274,7 +4313,7 @@ public function testFindOne(): void public function testFindNull(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::isNull('nullable'), ]); @@ -4283,7 +4322,7 @@ public function testFindNull(): void public function testFindNotNull(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::isNotNull('nullable'), ]); @@ -4292,18 +4331,18 @@ public function testFindNotNull(): void public function testFindStartsWith(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::startsWith('name', 'Work'), ]); $this->assertEquals(2, count($documents)); if ($this->getDatabase()->getAdapter() instanceof SQL) { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::startsWith('name', '%ork'), ]); } else { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::startsWith('name', '.*ork'), ]); } @@ -4313,7 +4352,7 @@ public function testFindStartsWith(): void public function testFindStartsWithWords(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::startsWith('name', 'Work in Progress'), ]); @@ -4322,7 +4361,7 @@ public function testFindStartsWithWords(): void public function testFindEndsWith(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::endsWith('name', 'Marvel'), ]); @@ -4331,7 +4370,7 @@ public function testFindEndsWith(): void public function testFindSelect(): void { - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year']) ]); @@ -4349,7 +4388,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('$permissions', $document); } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year', '$id']) ]); @@ -4367,7 +4406,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('$permissions', $document); } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year', '$internalId']) ]); @@ -4385,7 +4424,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('$permissions', $document); } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year', '$collection']) ]); @@ -4403,7 +4442,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('$permissions', $document); } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year', '$createdAt']) ]); @@ -4421,7 +4460,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('$permissions', $document); } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year', '$updatedAt']) ]); @@ -4439,7 +4478,7 @@ public function testFindSelect(): void $this->assertArrayNotHasKey('$permissions', $document); } - $documents = $this->getDatabase()->find('movies', [ + $documents = static::getDatabase()->find('movies', [ Query::select(['name', 'year', '$permissions']) ]); @@ -4463,39 +4502,40 @@ public function testFindSelect(): void */ public function testCount(): void { - $count = $this->getDatabase()->count('movies'); + $count = static::getDatabase()->count('movies'); $this->assertEquals(6, $count); - $count = $this->getDatabase()->count('movies', [Query::equal('year', [2019])]); + $count = static::getDatabase()->count('movies', [Query::equal('year', [2019])]); + $this->assertEquals(2, $count); - $count = $this->getDatabase()->count('movies', [Query::equal('with-dash', ['Works'])]); + $count = static::getDatabase()->count('movies', [Query::equal('with-dash', ['Works'])]); $this->assertEquals(2, $count); - $count = $this->getDatabase()->count('movies', [Query::equal('with-dash', ['Works2', 'Works3'])]); + $count = static::getDatabase()->count('movies', [Query::equal('with-dash', ['Works2', 'Works3'])]); $this->assertEquals(4, $count); - self::$authorization->removeRole('user:x'); - $count = $this->getDatabase()->count('movies'); + Authorization::unsetRole('user:x'); + $count = static::getDatabase()->count('movies'); $this->assertEquals(5, $count); - self::$authorization->disable(); - $count = $this->getDatabase()->count('movies'); + Authorization::disable(); + $count = static::getDatabase()->count('movies'); $this->assertEquals(6, $count); - self::$authorization->reset(); + Authorization::reset(); - self::$authorization->disable(); - $count = $this->getDatabase()->count('movies', [], 3); + Authorization::disable(); + $count = static::getDatabase()->count('movies', [], 3); $this->assertEquals(3, $count); - self::$authorization->reset(); + Authorization::reset(); /** * Test that OR queries are handled correctly */ - self::$authorization->disable(); - $count = $this->getDatabase()->count('movies', [ + Authorization::disable(); + $count = static::getDatabase()->count('movies', [ Query::equal('director', ['TBD', 'Joe Johnston']), Query::equal('year', [2025]), ]); $this->assertEquals(1, $count); - self::$authorization->reset(); + Authorization::reset(); } /** @@ -4503,28 +4543,29 @@ public function testCount(): void */ public function testSum(): void { - self::$authorization->addRole('user:x'); - $sum = $this->getDatabase()->sum('movies', 'year', [Query::equal('year', [2019]),]); + Authorization::setRole('user:x'); + + $sum = static::getDatabase()->sum('movies', 'year', [Query::equal('year', [2019]),]); $this->assertEquals(2019 + 2019, $sum); - $sum = $this->getDatabase()->sum('movies', 'year'); + $sum = static::getDatabase()->sum('movies', 'year'); $this->assertEquals(2013 + 2019 + 2011 + 2019 + 2025 + 2026, $sum); - $sum = $this->getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); - $sum = $this->getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); - $sum = $this->getDatabase()->sum('movies', 'year', [Query::equal('year', [2019])], 1); + $sum = static::getDatabase()->sum('movies', 'year', [Query::equal('year', [2019])], 1); $this->assertEquals(2019, $sum); - self::$authorization->removeRole('user:x'); - self::$authorization->removeRole('userx'); - $sum = $this->getDatabase()->sum('movies', 'year', [Query::equal('year', [2019]),]); + Authorization::unsetRole('user:x'); + Authorization::unsetRole('userx'); + $sum = static::getDatabase()->sum('movies', 'year', [Query::equal('year', [2019]),]); $this->assertEquals(2019 + 2019, $sum); - $sum = $this->getDatabase()->sum('movies', 'year'); + $sum = static::getDatabase()->sum('movies', 'year'); $this->assertEquals(2013 + 2019 + 2011 + 2019 + 2025, $sum); - $sum = $this->getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); - $sum = $this->getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); } @@ -4719,7 +4760,7 @@ public function testEncodeDecode(): void ], ]); - $result = $this->getDatabase()->encode($collection, $document); + $result = static::getDatabase()->encode($collection, $document); $this->assertEquals('608fdbe51361a', $result->getAttribute('$id')); $this->assertContains('read("any")', $result->getAttribute('$permissions')); @@ -4743,7 +4784,7 @@ public function testEncodeDecode(): void $this->assertEquals(['admin', 'developer', 'tester',], $result->getAttribute('roles')); $this->assertEquals(['{"$id":"1","label":"x"}', '{"$id":"2","label":"y"}', '{"$id":"3","label":"z"}',], $result->getAttribute('tags')); - $result = $this->getDatabase()->decode($collection, $document); + $result = static::getDatabase()->decode($collection, $document); $this->assertEquals('608fdbe51361a', $result->getAttribute('$id')); $this->assertContains('read("any")', $result->getAttribute('$permissions')); @@ -4777,10 +4818,10 @@ public function testEncodeDecode(): void */ public function testReadPermissionsSuccess(Document $document): Document { - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); - $document = $this->getDatabase()->createDocument('documents', new Document([ + $document = static::getDatabase()->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -4800,22 +4841,22 @@ public function testReadPermissionsSuccess(Document $document): Document $this->assertEquals(false, $document->isEmpty()); - self::$authorization->cleanRoles(); + Authorization::cleanRoles(); - $document = $this->getDatabase()->getDocument($document->getCollection(), $document->getId()); + $document = static::getDatabase()->getDocument($document->getCollection(), $document->getId()); $this->assertEquals(true, $document->isEmpty()); - self::$authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); return $document; } public function testReadPermissionsFailure(): Document { - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); - $document = $this->getDatabase()->createDocument('documents', new Document([ + $document = static::getDatabase()->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::user('1')), Permission::create(Role::user('1')), @@ -4833,13 +4874,13 @@ public function testReadPermissionsFailure(): Document 'colors' => ['pink', 'green', 'blue'], ])); - self::$authorization->cleanRoles(); + Authorization::cleanRoles(); - $document = $this->getDatabase()->getDocument($document->getCollection(), $document->getId()); + $document = static::getDatabase()->getDocument($document->getCollection(), $document->getId()); $this->assertEquals(true, $document->isEmpty()); - self::$authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); return $document; } @@ -4849,10 +4890,10 @@ public function testReadPermissionsFailure(): Document */ public function testWritePermissionsSuccess(Document $document): void { - self::$authorization->cleanRoles(); + Authorization::cleanRoles(); $this->expectException(AuthorizationException::class); - $this->getDatabase()->createDocument('documents', new Document([ + static::getDatabase()->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -4878,10 +4919,10 @@ public function testWritePermissionsUpdateFailure(Document $document): Document { $this->expectException(AuthorizationException::class); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); - $document = $this->getDatabase()->createDocument('documents', new Document([ + $document = static::getDatabase()->createDocument('documents', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -4899,9 +4940,9 @@ public function testWritePermissionsUpdateFailure(Document $document): Document 'colors' => ['pink', 'green', 'blue'], ])); - self::$authorization->cleanRoles(); + Authorization::cleanRoles(); - $document = $this->getDatabase()->updateDocument('documents', $document->getId(), new Document([ + $document = static::getDatabase()->updateDocument('documents', $document->getId(), new Document([ '$id' => ID::custom($document->getId()), '$permissions' => [ Permission::read(Role::any()), @@ -4923,7 +4964,7 @@ public function testWritePermissionsUpdateFailure(Document $document): Document public function testNoChangeUpdateDocumentWithoutPermission(): Document { - $document = $this->getDatabase()->createDocument('documents', new Document([ + $document = static::getDatabase()->createDocument('documents', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()) @@ -4939,7 +4980,7 @@ public function testNoChangeUpdateDocumentWithoutPermission(): Document 'colors' => ['pink', 'green', 'blue'], ])); - $updatedDocument = $this->getDatabase()->updateDocument( + $updatedDocument = static::getDatabase()->updateDocument( 'documents', $document->getId(), $document @@ -4949,7 +4990,7 @@ public function testNoChangeUpdateDocumentWithoutPermission(): Document // It should also not throw any authorization exception without any permission because of no change. $this->assertEquals($updatedDocument->getUpdatedAt(), $document->getUpdatedAt()); - $document = $this->getDatabase()->createDocument('documents', new Document([ + $document = static::getDatabase()->createDocument('documents', new Document([ '$id' => ID::unique(), '$permissions' => [], 'string' => 'text📝', @@ -4965,7 +5006,7 @@ public function testNoChangeUpdateDocumentWithoutPermission(): Document // Should throw exception, because nothing was updated, but there was no read permission try { - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'documents', $document->getId(), $document @@ -4979,22 +5020,22 @@ public function testNoChangeUpdateDocumentWithoutPermission(): Document public function testStructureValidationAfterRelationsAttribute(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection("structure_1", [], [], [Permission::create(Role::any())]); - $this->getDatabase()->createCollection("structure_2", [], [], [Permission::create(Role::any())]); + static::getDatabase()->createCollection("structure_1", [], [], [Permission::create(Role::any())]); + static::getDatabase()->createCollection("structure_2", [], [], [Permission::create(Role::any())]); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: "structure_1", relatedCollection: "structure_2", type: Database::RELATION_ONE_TO_ONE, ); try { - $this->getDatabase()->createDocument('structure_1', new Document([ + static::getDatabase()->createDocument('structure_1', new Document([ '$permissions' => [ Permission::read(Role::any()), ], @@ -5002,14 +5043,14 @@ public function testStructureValidationAfterRelationsAttribute(): void 'name' => 'Frozen', // Unknown attribute 'name' after relation attribute ])); $this->fail('Failed to throw exception'); - } catch (Exception $e) { + } catch(Exception $e) { $this->assertInstanceOf(StructureException::class, $e); } } public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -5030,13 +5071,13 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void Permission::delete(Role::any()), ]; for ($i = 1; $i < 6; $i++) { - $this->getDatabase()->createCollection("level{$i}", [$attribute], [], $permissions); + static::getDatabase()->createCollection("level{$i}", [$attribute], [], $permissions); } for ($i = 1; $i < 5; $i++) { $collectionId = $i; $relatedCollectionId = $i + 1; - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: "level{$collectionId}", relatedCollection: "level{$relatedCollectionId}", type: Database::RELATION_ONE_TO_ONE, @@ -5045,7 +5086,7 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void } // Create document with relationship with nested data - $level1 = $this->getDatabase()->createDocument('level1', new Document([ + $level1 = static::getDatabase()->createDocument('level1', new Document([ '$id' => 'level1', '$permissions' => [], 'name' => 'Level 1', @@ -5070,18 +5111,18 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void ], ], ])); - $this->getDatabase()->updateDocument('level1', $level1->getId(), new Document($level1->getArrayCopy())); - $updatedLevel1 = $this->getDatabase()->getDocument('level1', $level1->getId()); + static::getDatabase()->updateDocument('level1', $level1->getId(), new Document($level1->getArrayCopy())); + $updatedLevel1 = static::getDatabase()->getDocument('level1', $level1->getId()); $this->assertEquals($level1, $updatedLevel1); try { - $this->getDatabase()->updateDocument('level1', $level1->getId(), $level1->setAttribute('name', 'haha')); + static::getDatabase()->updateDocument('level1', $level1->getId(), $level1->setAttribute('name', 'haha')); $this->fail('Failed to throw exception'); - } catch (Exception $e) { + } catch(Exception $e) { $this->assertInstanceOf(AuthorizationException::class, $e); } $level1->setAttribute('name', 'Level 1'); - $this->getDatabase()->updateCollection('level3', [ + static::getDatabase()->updateCollection('level3', [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), @@ -5094,11 +5135,11 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void $level2->setAttribute('level3', $level3); $level1->setAttribute('level2', $level2); - $level1 = $this->getDatabase()->updateDocument('level1', $level1->getId(), $level1); + $level1 = static::getDatabase()->updateDocument('level1', $level1->getId(), $level1); $this->assertEquals('updated value', $level1['level2']['level3']['name']); for ($i = 1; $i < 6; $i++) { - $this->getDatabase()->deleteCollection("level{$i}"); + static::getDatabase()->deleteCollection("level{$i}"); } } @@ -5120,10 +5161,10 @@ public function testExceptionAttributeLimit(): void ]); } - $this->getDatabase()->createCollection('attributeLimit', $attributes); + static::getDatabase()->createCollection('attributeLimit', $attributes); $this->expectException(LimitException::class); - $this->assertEquals(false, $this->getDatabase()->createAttribute('attributeLimit', "breaking", Database::VAR_INTEGER, 0, true)); + $this->assertEquals(false, static::getDatabase()->createAttribute('attributeLimit', "breaking", Database::VAR_INTEGER, 0, true)); } // Default assertion for other adapters @@ -5136,7 +5177,7 @@ public function testExceptionAttributeLimit(): void public function testCheckAttributeCountLimit(): void { if ($this->getDatabase()->getLimitForAttributes() > 0) { - $collection = $this->getDatabase()->getCollection('attributeLimit'); + $collection = static::getDatabase()->getCollection('attributeLimit'); // create same attribute in testExceptionAttributeLimit $attribute = new Document([ @@ -5151,7 +5192,7 @@ public function testCheckAttributeCountLimit(): void ]); $this->expectException(LimitException::class); - $this->assertEquals(false, $this->getDatabase()->checkAttribute($collection, $attribute)); + $this->assertEquals(false, static::getDatabase()->checkAttribute($collection, $attribute)); } // Default assertion for other adapters @@ -5187,7 +5228,7 @@ public function rowWidthExceedsMaximum(): array */ public function testExceptionWidthLimit(int $key, int $stringSize, int $stringCount, int $intCount, int $floatCount, int $boolCount): void { - if ($this->getDatabase()->getAdapter()::getDocumentSizeLimit() > 0) { + if (static::getDatabase()->getAdapter()::getDocumentSizeLimit() > 0) { $attributes = []; // Load the collection up to the limit @@ -5247,10 +5288,10 @@ public function testExceptionWidthLimit(int $key, int $stringSize, int $stringCo ]); } - $collection = $this->getDatabase()->createCollection("widthLimit{$key}", $attributes); + $collection = static::getDatabase()->createCollection("widthLimit{$key}", $attributes); $this->expectException(LimitException::class); - $this->assertEquals(false, $this->getDatabase()->createAttribute("widthLimit{$key}", "breaking", Database::VAR_STRING, 100, true)); + $this->assertEquals(false, static::getDatabase()->createAttribute("widthLimit{$key}", "breaking", Database::VAR_STRING, 100, true)); } // Default assertion for other adapters @@ -5263,8 +5304,8 @@ public function testExceptionWidthLimit(int $key, int $stringSize, int $stringCo */ public function testCheckAttributeWidthLimit(int $key, int $stringSize, int $stringCount, int $intCount, int $floatCount, int $boolCount): void { - if ($this->getDatabase()->getAdapter()::getDocumentSizeLimit() > 0) { - $collection = $this->getDatabase()->getCollection("widthLimit{$key}"); + if (static::getDatabase()->getAdapter()::getDocumentSizeLimit() > 0) { + $collection = static::getDatabase()->getCollection("widthLimit{$key}"); // create same attribute in testExceptionWidthLimit $attribute = new Document([ @@ -5279,7 +5320,7 @@ public function testCheckAttributeWidthLimit(int $key, int $stringSize, int $str ]); $this->expectException(LimitException::class); - $this->assertEquals(false, $this->getDatabase()->checkAttribute($collection, $attribute)); + $this->assertEquals(false, static::getDatabase()->checkAttribute($collection, $attribute)); } // Default assertion for other adapters @@ -5288,22 +5329,22 @@ public function testCheckAttributeWidthLimit(int $key, int $stringSize, int $str public function testExceptionIndexLimit(): void { - $this->getDatabase()->createCollection('indexLimit'); + static::getDatabase()->createCollection('indexLimit'); // add unique attributes for indexing for ($i = 0; $i < 64; $i++) { - $this->assertEquals(true, $this->getDatabase()->createAttribute('indexLimit', "test{$i}", Database::VAR_STRING, 16, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexLimit', "test{$i}", Database::VAR_STRING, 16, true)); } // Testing for indexLimit // Add up to the limit, then check if the next index throws IndexLimitException for ($i = 0; $i < ($this->getDatabase()->getLimitForIndexes()); $i++) { - $this->assertEquals(true, $this->getDatabase()->createIndex('indexLimit', "index{$i}", Database::INDEX_KEY, ["test{$i}"], [16])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexLimit', "index{$i}", Database::INDEX_KEY, ["test{$i}"], [16])); } $this->expectException(LimitException::class); - $this->assertEquals(false, $this->getDatabase()->createIndex('indexLimit', "index64", Database::INDEX_KEY, ["test64"], [16])); + $this->assertEquals(false, static::getDatabase()->createIndex('indexLimit', "index64", Database::INDEX_KEY, ["test64"], [16])); - $this->getDatabase()->deleteCollection('indexLimit'); + static::getDatabase()->deleteCollection('indexLimit'); } /** @@ -5312,10 +5353,9 @@ public function testExceptionIndexLimit(): void public function testExceptionDuplicate(Document $document): void { $document->setAttribute('$id', 'duplicated'); - $this->getDatabase()->createDocument($document->getCollection(), $document); - + static::getDatabase()->createDocument($document->getCollection(), $document); $this->expectException(DuplicateException::class); - $this->getDatabase()->createDocument($document->getCollection(), $document); + static::getDatabase()->createDocument($document->getCollection(), $document); } /** @@ -5325,12 +5365,12 @@ public function testExceptionCaseInsensitiveDuplicate(Document $document): Docum { $document->setAttribute('$id', 'caseSensitive'); $document->setAttribute('$internalId', '200'); - $this->getDatabase()->createDocument($document->getCollection(), $document); + static::getDatabase()->createDocument($document->getCollection(), $document); $document->setAttribute('$id', 'CaseSensitive'); $this->expectException(DuplicateException::class); - $this->getDatabase()->createDocument($document->getCollection(), $document); + static::getDatabase()->createDocument($document->getCollection(), $document); return $document; } @@ -5342,9 +5382,9 @@ public function testUniqueIndexDuplicate(): void { $this->expectException(DuplicateException::class); - $this->assertEquals(true, $this->getDatabase()->createIndex('movies', 'uniqueIndex', Database::INDEX_UNIQUE, ['name'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::getDatabase()->createIndex('movies', 'uniqueIndex', Database::INDEX_UNIQUE, ['name'], [128], [Database::ORDER_ASC])); - $this->getDatabase()->createDocument('movies', new Document([ + static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -5374,9 +5414,9 @@ public function testUniqueIndexDuplicate(): void */ public function testUniqueIndexDuplicateUpdate(): void { - self::$authorization->addRole(Role::users()->toString()); + Authorization::setRole(Role::users()->toString()); // create document then update to conflict with index - $document = $this->getDatabase()->createDocument('movies', new Document([ + $document = static::getDatabase()->createDocument('movies', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::read(Role::user('1')), @@ -5402,7 +5442,7 @@ public function testUniqueIndexDuplicateUpdate(): void $this->expectException(DuplicateException::class); - $this->getDatabase()->updateDocument('movies', $document->getId(), $document->setAttribute('name', 'Frozen')); + static::getDatabase()->updateDocument('movies', $document->getId(), $document->setAttribute('name', 'Frozen')); } public function testGetAttributeLimit(): void @@ -5428,7 +5468,7 @@ public function testGetId(): void public function testRenameIndex(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $numbers = $database->createCollection('numbers'); $database->createAttribute('numbers', 'verbose', Database::VAR_STRING, 128, true); @@ -5454,7 +5494,7 @@ public function testRenameIndex(): void */ public function testRenameIndexMissing(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $this->expectExceptionMessage('Index not found'); $index = $database->renameIndex('numbers', 'index1', 'index4'); } @@ -5465,14 +5505,14 @@ public function testRenameIndexMissing(): void */ public function testRenameIndexExisting(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $this->expectExceptionMessage('Index name already used'); $index = $database->renameIndex('numbers', 'index3', 'index2'); } public function testRenameAttribute(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $colors = $database->createCollection('colors'); $database->createAttribute('colors', 'name', Database::VAR_STRING, 128, true); @@ -5518,7 +5558,7 @@ public function testRenameAttribute(): void */ public function textRenameAttributeMissing(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $this->expectExceptionMessage('Attribute not found'); $database->renameAttribute('colors', 'name2', 'name3'); } @@ -5529,14 +5569,14 @@ public function textRenameAttributeMissing(): void */ public function testRenameAttributeExisting(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $this->expectExceptionMessage('Attribute name already used'); $database->renameAttribute('colors', 'verbose', 'hex'); } public function testUpdateAttributeDefault(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $flowers = $database->createCollection('flowers'); $database->createAttribute('flowers', 'name', Database::VAR_STRING, 128, true); @@ -5591,7 +5631,7 @@ public function testUpdateAttributeDefault(): void */ public function testUpdateAttributeRequired(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $database->updateAttributeRequired('flowers', 'inStock', true); @@ -5613,7 +5653,7 @@ public function testUpdateAttributeRequired(): void */ public function testUpdateAttributeFilter(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $database->createAttribute('flowers', 'cartModel', Database::VAR_STRING, 2000, false); @@ -5646,7 +5686,7 @@ public function testUpdateAttributeFilter(): void */ public function testUpdateAttributeFormat(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $database->createAttribute('flowers', 'price', Database::VAR_INTEGER, 0, false); @@ -5707,7 +5747,7 @@ public function testUpdateAttributeStructure(): void return new Range($min, $max); }, Database::VAR_INTEGER); - $database = $this->getDatabase(); + $database = static::getDatabase(); // price attribute $collection = $database->getCollection('flowers'); @@ -6031,27 +6071,27 @@ public function testUpdateAttributeSize(): void */ public function testCreatedAtUpdatedAtAssert(): void { - $document = $this->getDatabase()->getDocument('created_at', 'uid123'); + $document = static::getDatabase()->getDocument('created_at', 'uid123'); $this->assertEquals(true, !$document->isEmpty()); sleep(1); $document->setAttribute('title', 'new title'); - $this->getDatabase()->updateDocument('created_at', 'uid123', $document); - $document = $this->getDatabase()->getDocument('created_at', 'uid123'); + static::getDatabase()->updateDocument('created_at', 'uid123', $document); + $document = static::getDatabase()->getDocument('created_at', 'uid123'); $this->assertGreaterThan($document->getCreatedAt(), $document->getUpdatedAt()); $this->expectException(DuplicateException::class); - $this->getDatabase()->createCollection('created_at'); + static::getDatabase()->createCollection('created_at'); } public function testCreateDatetime(): void { - $this->getDatabase()->createCollection('datetime'); + static::getDatabase()->createCollection('datetime'); - $this->assertEquals(true, $this->getDatabase()->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); - $this->assertEquals(true, $this->getDatabase()->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); + $this->assertEquals(true, static::getDatabase()->createAttribute('datetime', 'date', Database::VAR_DATETIME, 0, true, null, true, false, null, [], ['datetime'])); + $this->assertEquals(true, static::getDatabase()->createAttribute('datetime', 'date2', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); - $doc = $this->getDatabase()->createDocument('datetime', new Document([ + $doc = static::getDatabase()->createDocument('datetime', new Document([ '$id' => ID::custom('id1234'), '$permissions' => [ Permission::read(Role::any()), @@ -6069,25 +6109,25 @@ public function testCreateDatetime(): void $this->assertGreaterThan('2020-08-16T19:30:08.363+00:00', $doc->getCreatedAt()); $this->assertGreaterThan('2020-08-16T19:30:08.363+00:00', $doc->getUpdatedAt()); - $document = $this->getDatabase()->getDocument('datetime', 'id1234'); + $document = static::getDatabase()->getDocument('datetime', 'id1234'); $dateValidator = new DatetimeValidator(); $this->assertEquals(null, $document->getAttribute('date2')); $this->assertEquals(true, $dateValidator->isValid($document->getAttribute('date'))); $this->assertEquals(false, $dateValidator->isValid($document->getAttribute('date2'))); - $documents = $this->getDatabase()->find('datetime', [ + $documents = static::getDatabase()->find('datetime', [ Query::greaterThan('date', '1975-12-06 10:00:00+01:00'), Query::lessThan('date', '2030-12-06 10:00:00-01:00'), ]); $this->assertEquals(1, count($documents)); - $documents = $this->getDatabase()->find('datetime', [ + $documents = static::getDatabase()->find('datetime', [ Query::greaterThan('$createdAt', '1975-12-06 11:00:00.000'), ]); $this->assertCount(1, $documents); try { - $this->getDatabase()->createDocument('datetime', new Document([ + static::getDatabase()->createDocument('datetime', new Document([ 'date' => "1975-12-06 00:00:61" // 61 seconds is invalid ])); $this->fail('Failed to throw exception'); @@ -6112,7 +6152,7 @@ public function testCreateDatetime(): void foreach ($invalidDates as $date) { try { - $this->getDatabase()->find('datetime', [ + static::getDatabase()->find('datetime', [ Query::equal('date', [$date]) ]); $this->fail('Failed to throw exception'); @@ -6125,16 +6165,16 @@ public function testCreateDatetime(): void public function testCreateDateTimeAttributeFailure(): void { - $this->getDatabase()->createCollection('datetime_fail'); + static::getDatabase()->createCollection('datetime_fail'); /** Test for FAILURE */ $this->expectException(Exception::class); - $this->getDatabase()->createAttribute('datetime_fail', 'date_fail', Database::VAR_DATETIME, 0, false); + static::getDatabase()->createAttribute('datetime_fail', 'date_fail', Database::VAR_DATETIME, 0, false); } public function testKeywords(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $keywords = $database->getKeywords(); // Collection name tests @@ -6196,7 +6236,7 @@ public function testKeywords(): void $collection = $database->createCollection($collectionName); $this->assertEquals($collectionName, $collection->getId()); - $attribute = $this->getDatabase()->createAttribute($collectionName, $keyword, Database::VAR_STRING, 128, true); + $attribute = static::getDatabase()->createAttribute($collectionName, $keyword, Database::VAR_STRING, 128, true); $this->assertEquals(true, $attribute); $document = new Document([ @@ -6245,8 +6285,8 @@ public function testKeywords(): void public function testWritePermissions(): void { - self::$authorization->addRole(Role::any()->toString()); - $database = $this->getDatabase(); + Authorization::setRole(Role::any()->toString()); + $database = static::getDatabase(); $database->createCollection('animals', permissions: [ Permission::create(Role::any()), @@ -6311,7 +6351,7 @@ public function testWritePermissions(): void $newCat = $cat->setAttribute('type', 'newCat'); $database->updateDocument('animals', 'cat', $newCat); - $docs = self::$authorization->skip(fn () => $database->find('animals')); + $docs = Authorization::skip(fn () => $database->find('animals')); $this->assertCount(1, $docs); $this->assertEquals('cat', $docs[0]['$id']); $this->assertEquals('newCat', $docs[0]['type']); @@ -6319,19 +6359,19 @@ public function testWritePermissions(): void public function testNoInvalidKeysWithRelationships(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('species'); - $this->getDatabase()->createCollection('creatures'); - $this->getDatabase()->createCollection('characterstics'); + static::getDatabase()->createCollection('species'); + static::getDatabase()->createCollection('creatures'); + static::getDatabase()->createCollection('characterstics'); - $this->getDatabase()->createAttribute('species', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('creatures', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('characterstics', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('species', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('creatures', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('characterstics', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'species', relatedCollection: 'creatures', type: Database::RELATION_ONE_TO_ONE, @@ -6339,7 +6379,7 @@ public function testNoInvalidKeysWithRelationships(): void id: 'creature', twoWayKey:'species' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'creatures', relatedCollection: 'characterstics', type: Database::RELATION_ONE_TO_ONE, @@ -6348,7 +6388,7 @@ public function testNoInvalidKeysWithRelationships(): void twoWayKey:'creature' ); - $species = $this->getDatabase()->createDocument('species', new Document([ + $species = static::getDatabase()->createDocument('species', new Document([ '$id' => ID::custom('1'), '$permissions' => [ Permission::read(Role::any()), @@ -6370,7 +6410,7 @@ public function testNoInvalidKeysWithRelationships(): void ] ] ])); - $this->getDatabase()->updateDocument('species', $species->getId(), new Document([ + static::getDatabase()->updateDocument('species', $species->getId(), new Document([ '$id' => ID::custom('1'), '$collection' => 'species', 'creature' => [ @@ -6383,33 +6423,33 @@ public function testNoInvalidKeysWithRelationships(): void ] ] ])); - $updatedSpecies = $this->getDatabase()->getDocument('species', $species->getId()); + $updatedSpecies = static::getDatabase()->getDocument('species', $species->getId()); $this->assertEquals($species, $updatedSpecies); } // Relationships public function testOneToOneOneWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('person'); - $this->getDatabase()->createCollection('library'); + static::getDatabase()->createCollection('person'); + static::getDatabase()->createCollection('library'); - $this->getDatabase()->createAttribute('person', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('library', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('library', 'area', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('person', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('library', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('library', 'area', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'person', relatedCollection: 'library', type: Database::RELATION_ONE_TO_ONE ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('person'); + $collection = static::getDatabase()->getCollection('person'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { @@ -6425,14 +6465,14 @@ public function testOneToOneOneWayRelationship(): void } try { - $this->getDatabase()->deleteAttribute('person', 'library'); + static::getDatabase()->deleteAttribute('person', 'library'); $this->fail('Failed to throw Exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete relationship as an attribute', $e->getMessage()); } // Create document with relationship with nested data - $person1 = $this->getDatabase()->createDocument('person', new Document([ + $person1 = static::getDatabase()->createDocument('person', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -6453,24 +6493,24 @@ public function testOneToOneOneWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'person', 'person1', $person1->setAttribute('library', 'no-library') ); - $person1Document = $this->getDatabase()->getDocument('person', 'person1'); + $person1Document = static::getDatabase()->getDocument('person', 'person1'); // Assert document does not contain non existing relation document. $this->assertEquals(null, $person1Document->getAttribute('library')); - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'person', 'person1', $person1->setAttribute('library', 'library1') ); // Update through create - $library10 = $this->getDatabase()->createDocument('library', new Document([ + $library10 = static::getDatabase()->createDocument('library', new Document([ '$id' => 'library10', '$permissions' => [ Permission::read(Role::any()), @@ -6479,7 +6519,7 @@ public function testOneToOneOneWayRelationship(): void 'name' => 'Library 10', 'area' => 'Area 10', ])); - $person10 = $this->getDatabase()->createDocument('person', new Document([ + $person10 = static::getDatabase()->createDocument('person', new Document([ '$id' => 'person10', '$permissions' => [ Permission::read(Role::any()), @@ -6494,11 +6534,11 @@ public function testOneToOneOneWayRelationship(): void ], ])); $this->assertEquals('Library 10 Updated', $person10->getAttribute('library')->getAttribute('name')); - $library10 = $this->getDatabase()->getDocument('library', $library10->getId()); + $library10 = static::getDatabase()->getDocument('library', $library10->getId()); $this->assertEquals('Library 10 Updated', $library10->getAttribute('name')); // Create document with relationship with related ID - $this->getDatabase()->createDocument('library', new Document([ + static::getDatabase()->createDocument('library', new Document([ '$id' => 'library2', '$permissions' => [ Permission::read(Role::any()), @@ -6507,7 +6547,7 @@ public function testOneToOneOneWayRelationship(): void 'name' => 'Library 2', 'area' => 'Area 2', ])); - $this->getDatabase()->createDocument('person', new Document([ + static::getDatabase()->createDocument('person', new Document([ '$id' => 'person2', '$permissions' => [ Permission::read(Role::any()), @@ -6519,34 +6559,34 @@ public function testOneToOneOneWayRelationship(): void ])); // Get documents with relationship - $person1 = $this->getDatabase()->getDocument('person', 'person1'); + $person1 = static::getDatabase()->getDocument('person', 'person1'); $library = $person1->getAttribute('library'); $this->assertEquals('library1', $library['$id']); $this->assertArrayNotHasKey('person', $library); - $person = $this->getDatabase()->getDocument('person', 'person2'); + $person = static::getDatabase()->getDocument('person', 'person2'); $library = $person->getAttribute('library'); $this->assertEquals('library2', $library['$id']); $this->assertArrayNotHasKey('person', $library); // Get related documents - $library = $this->getDatabase()->getDocument('library', 'library1'); + $library = static::getDatabase()->getDocument('library', 'library1'); $this->assertArrayNotHasKey('person', $library); - $library = $this->getDatabase()->getDocument('library', 'library2'); + $library = static::getDatabase()->getDocument('library', 'library2'); $this->assertArrayNotHasKey('person', $library); - $people = $this->getDatabase()->find('person', [ + $people = static::getDatabase()->find('person', [ Query::select(['name']) ]); $this->assertArrayNotHasKey('library', $people[0]); - $people = $this->getDatabase()->find('person'); + $people = static::getDatabase()->find('person'); $this->assertEquals(3, \count($people)); // Select related document attributes - $person = $this->getDatabase()->findOne('person', [ + $person = static::getDatabase()->findOne('person', [ Query::select(['*', 'library.name']) ]); @@ -6557,7 +6597,7 @@ public function testOneToOneOneWayRelationship(): void $this->assertEquals('Library 1', $person->getAttribute('library')->getAttribute('name')); $this->assertArrayNotHasKey('area', $person->getAttribute('library')); - $person = $this->getDatabase()->getDocument('person', 'person1', [ + $person = static::getDatabase()->getDocument('person', 'person1', [ Query::select(['*', 'library.name', '$id']) ]); @@ -6566,36 +6606,36 @@ public function testOneToOneOneWayRelationship(): void - $document = $this->getDatabase()->getDocument('person', $person->getId(), [ + $document = static::getDatabase()->getDocument('person', $person->getId(), [ Query::select(['name']), ]); $this->assertArrayNotHasKey('library', $document); $this->assertEquals('Person 1', $document['name']); - $document = $this->getDatabase()->getDocument('person', $person->getId(), [ + $document = static::getDatabase()->getDocument('person', $person->getId(), [ Query::select(['*']), ]); $this->assertEquals('library1', $document['library']); - $document = $this->getDatabase()->getDocument('person', $person->getId(), [ + $document = static::getDatabase()->getDocument('person', $person->getId(), [ Query::select(['library.*']), ]); $this->assertEquals('Library 1', $document['library']['name']); $this->assertArrayNotHasKey('name', $document); // Update root document attribute without altering relationship - $person1 = $this->getDatabase()->updateDocument( + $person1 = static::getDatabase()->updateDocument( 'person', $person1->getId(), $person1->setAttribute('name', 'Person 1 Updated') ); $this->assertEquals('Person 1 Updated', $person1->getAttribute('name')); - $person1 = $this->getDatabase()->getDocument('person', 'person1'); + $person1 = static::getDatabase()->getDocument('person', 'person1'); $this->assertEquals('Person 1 Updated', $person1->getAttribute('name')); // Update nested document attribute - $person1 = $this->getDatabase()->updateDocument( + $person1 = static::getDatabase()->updateDocument( 'person', $person1->getId(), $person1->setAttribute( @@ -6607,11 +6647,11 @@ public function testOneToOneOneWayRelationship(): void ); $this->assertEquals('Library 1 Updated', $person1->getAttribute('library')->getAttribute('name')); - $person1 = $this->getDatabase()->getDocument('person', 'person1'); + $person1 = static::getDatabase()->getDocument('person', 'person1'); $this->assertEquals('Library 1 Updated', $person1->getAttribute('library')->getAttribute('name')); // Create new document with no relationship - $person3 = $this->getDatabase()->createDocument('person', new Document([ + $person3 = static::getDatabase()->createDocument('person', new Document([ '$id' => 'person3', '$permissions' => [ Permission::read(Role::any()), @@ -6622,7 +6662,7 @@ public function testOneToOneOneWayRelationship(): void ])); // Update to relate to created document - $person3 = $this->getDatabase()->updateDocument( + $person3 = static::getDatabase()->updateDocument( 'person', $person3->getId(), $person3->setAttribute('library', new Document([ @@ -6637,23 +6677,23 @@ public function testOneToOneOneWayRelationship(): void ); $this->assertEquals('library3', $person3->getAttribute('library')['$id']); - $person3 = $this->getDatabase()->getDocument('person', 'person3'); + $person3 = static::getDatabase()->getDocument('person', 'person3'); $this->assertEquals('Library 3', $person3['library']['name']); - $libraryDocument = $this->getDatabase()->getDocument('library', 'library3'); + $libraryDocument = static::getDatabase()->getDocument('library', 'library3'); $libraryDocument->setAttribute('name', 'Library 3 updated'); - $this->getDatabase()->updateDocument('library', 'library3', $libraryDocument); - $libraryDocument = $this->getDatabase()->getDocument('library', 'library3'); + static::getDatabase()->updateDocument('library', 'library3', $libraryDocument); + $libraryDocument = static::getDatabase()->getDocument('library', 'library3'); $this->assertEquals('Library 3 updated', $libraryDocument['name']); - $person3 = $this->getDatabase()->getDocument('person', 'person3'); + $person3 = static::getDatabase()->getDocument('person', 'person3'); // Todo: This is failing $this->assertEquals($libraryDocument['name'], $person3['library']['name']); $this->assertEquals('library3', $person3->getAttribute('library')['$id']); // One to one can't relate to multiple documents, unique index throws duplicate try { - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'person', $person1->getId(), $person1->setAttribute('library', 'library2') @@ -6664,7 +6704,7 @@ public function testOneToOneOneWayRelationship(): void } // Create new document - $library4 = $this->getDatabase()->createDocument('library', new Document([ + $library4 = static::getDatabase()->createDocument('library', new Document([ '$id' => 'library4', '$permissions' => [ Permission::read(Role::any()), @@ -6675,33 +6715,33 @@ public function testOneToOneOneWayRelationship(): void ])); // Relate existing document to new document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'person', $person1->getId(), $person1->setAttribute('library', 'library4') ); // Relate existing document to new document as nested data - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'person', $person1->getId(), $person1->setAttribute('library', $library4) ); // Rename relationship key - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'person', id: 'library', newKey: 'newLibrary' ); // Get document with again - $person = $this->getDatabase()->getDocument('person', 'person1'); + $person = static::getDatabase()->getDocument('person', 'person1'); $library = $person->getAttribute('newLibrary'); $this->assertEquals('library4', $library['$id']); // Create person with no relationship - $this->getDatabase()->createDocument('person', new Document([ + static::getDatabase()->createDocument('person', new Document([ '$id' => 'person4', '$permissions' => [ Permission::read(Role::any()), @@ -6712,22 +6752,22 @@ public function testOneToOneOneWayRelationship(): void ])); // Can delete parent document with no relation with on delete set to restrict - $deleted = $this->getDatabase()->deleteDocument('person', 'person4'); + $deleted = static::getDatabase()->deleteDocument('person', 'person4'); $this->assertEquals(true, $deleted); - $person4 = $this->getDatabase()->getDocument('person', 'person4'); + $person4 = static::getDatabase()->getDocument('person', 'person4'); $this->assertEquals(true, $person4->isEmpty()); // Cannot delete document while still related to another with on delete set to restrict try { - $this->getDatabase()->deleteDocument('person', 'person1'); + static::getDatabase()->deleteDocument('person', 'person1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Can delete child document while still related to another with on delete set to restrict - $person5 = $this->getDatabase()->createDocument('person', new Document([ + $person5 = static::getDatabase()->createDocument('person', new Document([ '$id' => 'person5', '$permissions' => [ Permission::read(Role::any()), @@ -6745,60 +6785,60 @@ public function testOneToOneOneWayRelationship(): void 'area' => 'Area 5', ], ])); - $deleted = $this->getDatabase()->deleteDocument('library', 'library5'); + $deleted = static::getDatabase()->deleteDocument('library', 'library5'); $this->assertEquals(true, $deleted); - $person5 = $this->getDatabase()->getDocument('person', 'person5'); + $person5 = static::getDatabase()->getDocument('person', 'person5'); $this->assertEquals(null, $person5->getAttribute('newLibrary')); // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'person', id: 'newLibrary', onDelete: Database::RELATION_MUTATE_SET_NULL ); // Delete parent, no effect on children for one-way - $this->getDatabase()->deleteDocument('person', 'person1'); + static::getDatabase()->deleteDocument('person', 'person1'); // Delete child, set parent relating attribute to null for one-way - $this->getDatabase()->deleteDocument('library', 'library2'); + static::getDatabase()->deleteDocument('library', 'library2'); // Check relation was set to null - $person2 = $this->getDatabase()->getDocument('person', 'person2'); + $person2 = static::getDatabase()->getDocument('person', 'person2'); $this->assertEquals(null, $person2->getAttribute('newLibrary', '')); // Relate to another document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'person', $person2->getId(), $person2->setAttribute('newLibrary', 'library4') ); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'person', id: 'newLibrary', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete parent, will delete child - $this->getDatabase()->deleteDocument('person', 'person2'); + static::getDatabase()->deleteDocument('person', 'person2'); // Check parent and child were deleted - $person = $this->getDatabase()->getDocument('person', 'person2'); + $person = static::getDatabase()->getDocument('person', 'person2'); $this->assertEquals(true, $person->isEmpty()); - $library = $this->getDatabase()->getDocument('library', 'library4'); + $library = static::getDatabase()->getDocument('library', 'library4'); $this->assertEquals(true, $library->isEmpty()); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'person', 'newLibrary' ); // Check parent doesn't have relationship anymore - $person = $this->getDatabase()->getDocument('person', 'person1'); + $person = static::getDatabase()->getDocument('person', 'person1'); $library = $person->getAttribute('newLibrary', ''); $this->assertEquals(null, $library); } @@ -6812,26 +6852,26 @@ public function testOneToOneOneWayRelationship(): void */ public function testOneToOneTwoWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('country'); - $this->getDatabase()->createCollection('city'); + static::getDatabase()->createCollection('country'); + static::getDatabase()->createCollection('city'); - $this->getDatabase()->createAttribute('country', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('city', 'code', Database::VAR_STRING, 3, true); - $this->getDatabase()->createAttribute('city', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('country', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('city', 'code', Database::VAR_STRING, 3, true); + static::getDatabase()->createAttribute('city', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'country', relatedCollection: 'city', type: Database::RELATION_ONE_TO_ONE, twoWay: true ); - $collection = $this->getDatabase()->getCollection('country'); + $collection = static::getDatabase()->getCollection('country'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'city') { @@ -6845,7 +6885,7 @@ public function testOneToOneTwoWayRelationship(): void } } - $collection = $this->getDatabase()->getCollection('city'); + $collection = static::getDatabase()->getCollection('city'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'country') { @@ -6880,40 +6920,40 @@ public function testOneToOneTwoWayRelationship(): void ], ]); - $this->getDatabase()->createDocument('country', new Document($doc->getArrayCopy())); - $country1 = $this->getDatabase()->getDocument('country', 'country1'); + static::getDatabase()->createDocument('country', new Document($doc->getArrayCopy())); + $country1 = static::getDatabase()->getDocument('country', 'country1'); $this->assertEquals('London', $country1->getAttribute('city')->getAttribute('name')); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('country', 'country1', (new Document($doc->getArrayCopy()))->setAttribute('city', 'no-city')); + static::getDatabase()->updateDocument('country', 'country1', (new Document($doc->getArrayCopy()))->setAttribute('city', 'no-city')); - $country1Document = $this->getDatabase()->getDocument('country', 'country1'); + $country1Document = static::getDatabase()->getDocument('country', 'country1'); // Assert document does not contain non existing relation document. $this->assertEquals(null, $country1Document->getAttribute('city')); - $this->getDatabase()->updateDocument('country', 'country1', (new Document($doc->getArrayCopy()))->setAttribute('city', 'city1')); + static::getDatabase()->updateDocument('country', 'country1', (new Document($doc->getArrayCopy()))->setAttribute('city', 'city1')); try { - $this->getDatabase()->deleteDocument('country', 'country1'); + static::getDatabase()->deleteDocument('country', 'country1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertInstanceOf(RestrictedException::class, $e); } - $this->assertTrue($this->getDatabase()->deleteDocument('city', 'city1')); + $this->assertTrue(static::getDatabase()->deleteDocument('city', 'city1')); - $city1 = $this->getDatabase()->getDocument('city', 'city1'); + $city1 = static::getDatabase()->getDocument('city', 'city1'); $this->assertTrue($city1->isEmpty()); - $country1 = $this->getDatabase()->getDocument('country', 'country1'); + $country1 = static::getDatabase()->getDocument('country', 'country1'); $this->assertTrue($country1->getAttribute('city')->isEmpty()); - $this->assertTrue($this->getDatabase()->deleteDocument('country', 'country1')); + $this->assertTrue(static::getDatabase()->deleteDocument('country', 'country1')); - $this->getDatabase()->createDocument('country', new Document($doc->getArrayCopy())); - $country1 = $this->getDatabase()->getDocument('country', 'country1'); + static::getDatabase()->createDocument('country', new Document($doc->getArrayCopy())); + $country1 = static::getDatabase()->getDocument('country', 'country1'); $this->assertEquals('London', $country1->getAttribute('city')->getAttribute('name')); // Create document with relationship with related ID - $this->getDatabase()->createDocument('city', new Document([ + static::getDatabase()->createDocument('city', new Document([ '$id' => 'city2', '$permissions' => [ Permission::read(Role::any()), @@ -6923,7 +6963,7 @@ public function testOneToOneTwoWayRelationship(): void 'name' => 'Paris', 'code' => 'PAR', ])); - $this->getDatabase()->createDocument('country', new Document([ + static::getDatabase()->createDocument('country', new Document([ '$id' => 'country2', '$permissions' => [ Permission::read(Role::any()), @@ -6935,7 +6975,7 @@ public function testOneToOneTwoWayRelationship(): void ])); // Create from child side - $this->getDatabase()->createDocument('city', new Document([ + static::getDatabase()->createDocument('city', new Document([ '$id' => 'city3', '$permissions' => [ Permission::read(Role::any()), @@ -6954,7 +6994,7 @@ public function testOneToOneTwoWayRelationship(): void 'name' => 'New Zealand', ], ])); - $this->getDatabase()->createDocument('country', new Document([ + static::getDatabase()->createDocument('country', new Document([ '$id' => 'country4', '$permissions' => [ Permission::read(Role::any()), @@ -6963,7 +7003,7 @@ public function testOneToOneTwoWayRelationship(): void ], 'name' => 'Australia', ])); - $this->getDatabase()->createDocument('city', new Document([ + static::getDatabase()->createDocument('city', new Document([ '$id' => 'city4', '$permissions' => [ Permission::read(Role::any()), @@ -6976,53 +7016,53 @@ public function testOneToOneTwoWayRelationship(): void ])); // Get document with relationship - $city = $this->getDatabase()->getDocument('city', 'city1'); + $city = static::getDatabase()->getDocument('city', 'city1'); $country = $city->getAttribute('country'); $this->assertEquals('country1', $country['$id']); $this->assertArrayNotHasKey('city', $country); - $city = $this->getDatabase()->getDocument('city', 'city2'); + $city = static::getDatabase()->getDocument('city', 'city2'); $country = $city->getAttribute('country'); $this->assertEquals('country2', $country['$id']); $this->assertArrayNotHasKey('city', $country); - $city = $this->getDatabase()->getDocument('city', 'city3'); + $city = static::getDatabase()->getDocument('city', 'city3'); $country = $city->getAttribute('country'); $this->assertEquals('country3', $country['$id']); $this->assertArrayNotHasKey('city', $country); - $city = $this->getDatabase()->getDocument('city', 'city4'); + $city = static::getDatabase()->getDocument('city', 'city4'); $country = $city->getAttribute('country'); $this->assertEquals('country4', $country['$id']); $this->assertArrayNotHasKey('city', $country); // Get inverse document with relationship - $country = $this->getDatabase()->getDocument('country', 'country1'); + $country = static::getDatabase()->getDocument('country', 'country1'); $city = $country->getAttribute('city'); $this->assertEquals('city1', $city['$id']); $this->assertArrayNotHasKey('country', $city); - $country = $this->getDatabase()->getDocument('country', 'country2'); + $country = static::getDatabase()->getDocument('country', 'country2'); $city = $country->getAttribute('city'); $this->assertEquals('city2', $city['$id']); $this->assertArrayNotHasKey('country', $city); - $country = $this->getDatabase()->getDocument('country', 'country3'); + $country = static::getDatabase()->getDocument('country', 'country3'); $city = $country->getAttribute('city'); $this->assertEquals('city3', $city['$id']); $this->assertArrayNotHasKey('country', $city); - $country = $this->getDatabase()->getDocument('country', 'country4'); + $country = static::getDatabase()->getDocument('country', 'country4'); $city = $country->getAttribute('city'); $this->assertEquals('city4', $city['$id']); $this->assertArrayNotHasKey('country', $city); - $countries = $this->getDatabase()->find('country'); + $countries = static::getDatabase()->find('country'); $this->assertEquals(4, \count($countries)); // Select related document attributes - $country = $this->getDatabase()->findOne('country', [ + $country = static::getDatabase()->findOne('country', [ Query::select(['*', 'city.name']) ]); @@ -7033,41 +7073,41 @@ public function testOneToOneTwoWayRelationship(): void $this->assertEquals('London', $country->getAttribute('city')->getAttribute('name')); $this->assertArrayNotHasKey('code', $country->getAttribute('city')); - $country = $this->getDatabase()->getDocument('country', 'country1', [ + $country = static::getDatabase()->getDocument('country', 'country1', [ Query::select(['*', 'city.name']) ]); $this->assertEquals('London', $country->getAttribute('city')->getAttribute('name')); $this->assertArrayNotHasKey('code', $country->getAttribute('city')); - $country1 = $this->getDatabase()->getDocument('country', 'country1'); + $country1 = static::getDatabase()->getDocument('country', 'country1'); // Update root document attribute without altering relationship - $country1 = $this->getDatabase()->updateDocument( + $country1 = static::getDatabase()->updateDocument( 'country', $country1->getId(), $country1->setAttribute('name', 'Country 1 Updated') ); $this->assertEquals('Country 1 Updated', $country1->getAttribute('name')); - $country1 = $this->getDatabase()->getDocument('country', 'country1'); + $country1 = static::getDatabase()->getDocument('country', 'country1'); $this->assertEquals('Country 1 Updated', $country1->getAttribute('name')); - $city2 = $this->getDatabase()->getDocument('city', 'city2'); + $city2 = static::getDatabase()->getDocument('city', 'city2'); // Update inverse root document attribute without altering relationship - $city2 = $this->getDatabase()->updateDocument( + $city2 = static::getDatabase()->updateDocument( 'city', $city2->getId(), $city2->setAttribute('name', 'City 2 Updated') ); $this->assertEquals('City 2 Updated', $city2->getAttribute('name')); - $city2 = $this->getDatabase()->getDocument('city', 'city2'); + $city2 = static::getDatabase()->getDocument('city', 'city2'); $this->assertEquals('City 2 Updated', $city2->getAttribute('name')); // Update nested document attribute - $country1 = $this->getDatabase()->updateDocument( + $country1 = static::getDatabase()->updateDocument( 'country', $country1->getId(), $country1->setAttribute( @@ -7079,11 +7119,11 @@ public function testOneToOneTwoWayRelationship(): void ); $this->assertEquals('City 1 Updated', $country1->getAttribute('city')->getAttribute('name')); - $country1 = $this->getDatabase()->getDocument('country', 'country1'); + $country1 = static::getDatabase()->getDocument('country', 'country1'); $this->assertEquals('City 1 Updated', $country1->getAttribute('city')->getAttribute('name')); // Update inverse nested document attribute - $city2 = $this->getDatabase()->updateDocument( + $city2 = static::getDatabase()->updateDocument( 'city', $city2->getId(), $city2->setAttribute( @@ -7095,11 +7135,11 @@ public function testOneToOneTwoWayRelationship(): void ); $this->assertEquals('Country 2 Updated', $city2->getAttribute('country')->getAttribute('name')); - $city2 = $this->getDatabase()->getDocument('city', 'city2'); + $city2 = static::getDatabase()->getDocument('city', 'city2'); $this->assertEquals('Country 2 Updated', $city2->getAttribute('country')->getAttribute('name')); // Create new document with no relationship - $country5 = $this->getDatabase()->createDocument('country', new Document([ + $country5 = static::getDatabase()->createDocument('country', new Document([ '$id' => 'country5', '$permissions' => [ Permission::read(Role::any()), @@ -7110,7 +7150,7 @@ public function testOneToOneTwoWayRelationship(): void ])); // Update to relate to created document - $country5 = $this->getDatabase()->updateDocument( + $country5 = static::getDatabase()->updateDocument( 'country', $country5->getId(), $country5->setAttribute('city', new Document([ @@ -7125,11 +7165,11 @@ public function testOneToOneTwoWayRelationship(): void ); $this->assertEquals('city5', $country5->getAttribute('city')['$id']); - $country5 = $this->getDatabase()->getDocument('country', 'country5'); + $country5 = static::getDatabase()->getDocument('country', 'country5'); $this->assertEquals('city5', $country5->getAttribute('city')['$id']); // Create new document with no relationship - $city6 = $this->getDatabase()->createDocument('city', new Document([ + $city6 = static::getDatabase()->createDocument('city', new Document([ '$id' => 'city6', '$permissions' => [ Permission::read(Role::any()), @@ -7141,7 +7181,7 @@ public function testOneToOneTwoWayRelationship(): void ])); // Update to relate to created document - $city6 = $this->getDatabase()->updateDocument( + $city6 = static::getDatabase()->updateDocument( 'city', $city6->getId(), $city6->setAttribute('country', new Document([ @@ -7155,12 +7195,12 @@ public function testOneToOneTwoWayRelationship(): void ); $this->assertEquals('country6', $city6->getAttribute('country')['$id']); - $city6 = $this->getDatabase()->getDocument('city', 'city6'); + $city6 = static::getDatabase()->getDocument('city', 'city6'); $this->assertEquals('country6', $city6->getAttribute('country')['$id']); // One to one can't relate to multiple documents, unique index throws duplicate try { - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'country', $country1->getId(), $country1->setAttribute('city', 'city2') @@ -7170,21 +7210,21 @@ public function testOneToOneTwoWayRelationship(): void $this->assertInstanceOf(DuplicateException::class, $e); } - $city1 = $this->getDatabase()->getDocument('city', 'city1'); + $city1 = static::getDatabase()->getDocument('city', 'city1'); // Set relationship to null - $city1 = $this->getDatabase()->updateDocument( + $city1 = static::getDatabase()->updateDocument( 'city', $city1->getId(), $city1->setAttribute('country', null) ); $this->assertEquals(null, $city1->getAttribute('country')); - $city1 = $this->getDatabase()->getDocument('city', 'city1'); + $city1 = static::getDatabase()->getDocument('city', 'city1'); $this->assertEquals(null, $city1->getAttribute('country')); // Create a new city with no relation - $city7 = $this->getDatabase()->createDocument('city', new Document([ + $city7 = static::getDatabase()->createDocument('city', new Document([ '$id' => 'city7', '$permissions' => [ Permission::read(Role::any()), @@ -7196,21 +7236,21 @@ public function testOneToOneTwoWayRelationship(): void ])); // Update document with relation to new document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'country', $country1->getId(), $country1->setAttribute('city', 'city7') ); // Relate existing document to new document as nested data - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'country', $country1->getId(), $country1->setAttribute('city', $city7) ); // Create a new country with no relation - $this->getDatabase()->createDocument('country', new Document([ + static::getDatabase()->createDocument('country', new Document([ '$id' => 'country7', '$permissions' => [ Permission::read(Role::any()), @@ -7221,14 +7261,14 @@ public function testOneToOneTwoWayRelationship(): void ])); // Update inverse document with new related document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'city', $city1->getId(), $city1->setAttribute('country', 'country7') ); // Rename relationship keys on both sides - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'country', 'city', 'newCity', @@ -7236,17 +7276,17 @@ public function testOneToOneTwoWayRelationship(): void ); // Get document with new relationship key - $city = $this->getDatabase()->getDocument('city', 'city1'); + $city = static::getDatabase()->getDocument('city', 'city1'); $country = $city->getAttribute('newCountry'); $this->assertEquals('country7', $country['$id']); // Get inverse document with new relationship key - $country = $this->getDatabase()->getDocument('country', 'country7'); + $country = static::getDatabase()->getDocument('country', 'country7'); $city = $country->getAttribute('newCity'); $this->assertEquals('city1', $city['$id']); // Create a new country with no relation - $this->getDatabase()->createDocument('country', new Document([ + static::getDatabase()->createDocument('country', new Document([ '$id' => 'country8', '$permissions' => [ Permission::read(Role::any()), @@ -7257,87 +7297,87 @@ public function testOneToOneTwoWayRelationship(): void ])); // Can delete parent document with no relation with on delete set to restrict - $deleted = $this->getDatabase()->deleteDocument('country', 'country8'); + $deleted = static::getDatabase()->deleteDocument('country', 'country8'); $this->assertEquals(1, $deleted); - $country8 = $this->getDatabase()->getDocument('country', 'country8'); + $country8 = static::getDatabase()->getDocument('country', 'country8'); $this->assertEquals(true, $country8->isEmpty()); // Cannot delete document while still related to another with on delete set to restrict try { - $this->getDatabase()->deleteDocument('country', 'country1'); + static::getDatabase()->deleteDocument('country', 'country1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'country', id: 'newCity', onDelete: Database::RELATION_MUTATE_SET_NULL ); - $this->getDatabase()->updateDocument('city', 'city1', new Document(['newCountry' => null, '$id' => 'city1'])); - $city1 = $this->getDatabase()->getDocument('city', 'city1'); + static::getDatabase()->updateDocument('city', 'city1', new Document(['newCountry' => null, '$id' => 'city1'])); + $city1 = static::getDatabase()->getDocument('city', 'city1'); $this->assertNull($city1->getAttribute('newCountry')); // Check Delete TwoWay TRUE && RELATION_MUTATE_SET_NULL && related value NULL - $this->assertTrue($this->getDatabase()->deleteDocument('city', 'city1')); - $city1 = $this->getDatabase()->getDocument('city', 'city1'); + $this->assertTrue(static::getDatabase()->deleteDocument('city', 'city1')); + $city1 = static::getDatabase()->getDocument('city', 'city1'); $this->assertTrue($city1->isEmpty()); // Delete parent, will set child relationship to null for two-way - $this->getDatabase()->deleteDocument('country', 'country1'); + static::getDatabase()->deleteDocument('country', 'country1'); // Check relation was set to null - $city7 = $this->getDatabase()->getDocument('city', 'city7'); + $city7 = static::getDatabase()->getDocument('city', 'city7'); $this->assertEquals(null, $city7->getAttribute('country', '')); // Delete child, set parent relationship to null for two-way - $this->getDatabase()->deleteDocument('city', 'city2'); + static::getDatabase()->deleteDocument('city', 'city2'); // Check relation was set to null - $country2 = $this->getDatabase()->getDocument('country', 'country2'); + $country2 = static::getDatabase()->getDocument('country', 'country2'); $this->assertEquals(null, $country2->getAttribute('city', '')); // Relate again - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'city', $city7->getId(), $city7->setAttribute('newCountry', 'country2') ); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'country', id: 'newCity', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete parent, will delete child - $this->getDatabase()->deleteDocument('country', 'country7'); + static::getDatabase()->deleteDocument('country', 'country7'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('country', 'country7'); + $library = static::getDatabase()->getDocument('country', 'country7'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('city', 'city1'); + $library = static::getDatabase()->getDocument('city', 'city1'); $this->assertEquals(true, $library->isEmpty()); // Delete child, will delete parent for two-way - $this->getDatabase()->deleteDocument('city', 'city7'); + static::getDatabase()->deleteDocument('city', 'city7'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('city', 'city7'); + $library = static::getDatabase()->getDocument('city', 'city7'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('country', 'country2'); + $library = static::getDatabase()->getDocument('country', 'country2'); $this->assertEquals(true, $library->isEmpty()); // Create new document to check after deleting relationship - $this->getDatabase()->createDocument('city', new Document([ + static::getDatabase()->createDocument('city', new Document([ '$id' => 'city7', '$permissions' => [ Permission::read(Role::any()), @@ -7353,33 +7393,33 @@ public function testOneToOneTwoWayRelationship(): void ])); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'country', 'newCity' ); // Try to get document again - $country = $this->getDatabase()->getDocument('country', 'country4'); + $country = static::getDatabase()->getDocument('country', 'country4'); $city = $country->getAttribute('newCity'); $this->assertEquals(null, $city); // Try to get inverse document again - $city = $this->getDatabase()->getDocument('city', 'city7'); + $city = static::getDatabase()->getDocument('city', 'city7'); $country = $city->getAttribute('newCountry'); $this->assertEquals(null, $country); } public function testIdenticalTwoWayKeyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('parent'); - $this->getDatabase()->createCollection('child'); + static::getDatabase()->createCollection('parent'); + static::getDatabase()->createCollection('child'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'parent', relatedCollection: 'child', type: Database::RELATION_ONE_TO_ONE, @@ -7387,7 +7427,7 @@ public function testIdenticalTwoWayKeyRelationship(): void ); try { - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'parent', relatedCollection: 'child', type: Database::RELATION_ONE_TO_MANY, @@ -7398,7 +7438,7 @@ public function testIdenticalTwoWayKeyRelationship(): void $this->assertEquals('Related attribute already exists', $e->getMessage()); } - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'parent', relatedCollection: 'child', type: Database::RELATION_ONE_TO_MANY, @@ -7406,7 +7446,7 @@ public function testIdenticalTwoWayKeyRelationship(): void twoWayKey: 'parent_id' ); - $collection = $this->getDatabase()->getCollection('parent'); + $collection = static::getDatabase()->getCollection('parent'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'child1') { @@ -7418,7 +7458,7 @@ public function testIdenticalTwoWayKeyRelationship(): void } } - $this->getDatabase()->createDocument('parent', new Document([ + static::getDatabase()->createDocument('parent', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), @@ -7436,7 +7476,7 @@ public function testIdenticalTwoWayKeyRelationship(): void ], ])); - $documents = $this->getDatabase()->find('parent', []); + $documents = static::getDatabase()->find('parent', []); $document = array_pop($documents); $this->assertArrayHasKey('child1', $document); $this->assertEquals('foo', $document->getAttribute('child1')->getId()); @@ -7444,7 +7484,7 @@ public function testIdenticalTwoWayKeyRelationship(): void $this->assertEquals('bar', $document->getAttribute('children')[0]->getId()); try { - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'parent', id: 'children', newKey: 'child1' @@ -7455,7 +7495,7 @@ public function testIdenticalTwoWayKeyRelationship(): void } try { - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'parent', id: 'children', newTwoWayKey: 'parent' @@ -7468,19 +7508,19 @@ public function testIdenticalTwoWayKeyRelationship(): void public function testOneToManyOneWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('artist'); - $this->getDatabase()->createCollection('album'); + static::getDatabase()->createCollection('artist'); + static::getDatabase()->createCollection('album'); - $this->getDatabase()->createAttribute('artist', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('album', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('album', 'price', Database::VAR_FLOAT, 0, true); + static::getDatabase()->createAttribute('artist', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('album', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('album', 'price', Database::VAR_FLOAT, 0, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'artist', relatedCollection: 'album', type: Database::RELATION_ONE_TO_MANY, @@ -7488,7 +7528,7 @@ public function testOneToManyOneWayRelationship(): void ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('artist'); + $collection = static::getDatabase()->getCollection('artist'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { @@ -7504,7 +7544,7 @@ public function testOneToManyOneWayRelationship(): void } // Create document with relationship with nested data - $artist1 = $this->getDatabase()->createDocument('artist', new Document([ + $artist1 = static::getDatabase()->createDocument('artist', new Document([ '$id' => 'artist1', '$permissions' => [ Permission::read(Role::any()), @@ -7526,14 +7566,14 @@ public function testOneToManyOneWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('artist', 'artist1', $artist1->setAttribute('albums', ['album1', 'no-album'])); + static::getDatabase()->updateDocument('artist', 'artist1', $artist1->setAttribute('albums', ['album1', 'no-album'])); - $artist1Document = $this->getDatabase()->getDocument('artist', 'artist1'); + $artist1Document = static::getDatabase()->getDocument('artist', 'artist1'); // Assert document does not contain non existing relation document. $this->assertEquals(1, \count($artist1Document->getAttribute('albums'))); // Create document with relationship with related ID - $this->getDatabase()->createDocument('album', new Document([ + static::getDatabase()->createDocument('album', new Document([ '$id' => 'album2', '$permissions' => [ Permission::read(Role::any()), @@ -7543,7 +7583,7 @@ public function testOneToManyOneWayRelationship(): void 'name' => 'Album 2', 'price' => 19.99, ])); - $this->getDatabase()->createDocument('artist', new Document([ + static::getDatabase()->createDocument('artist', new Document([ '$id' => 'artist2', '$permissions' => [ Permission::read(Role::any()), @@ -7565,19 +7605,19 @@ public function testOneToManyOneWayRelationship(): void ] ])); - $documents = $this->getDatabase()->find('artist', [ + $documents = static::getDatabase()->find('artist', [ Query::select(['name']), Query::limit(1) ]); $this->assertArrayNotHasKey('albums', $documents[0]); // Get document with relationship - $artist = $this->getDatabase()->getDocument('artist', 'artist1'); + $artist = static::getDatabase()->getDocument('artist', 'artist1'); $albums = $artist->getAttribute('albums', []); $this->assertEquals('album1', $albums[0]['$id']); $this->assertArrayNotHasKey('artist', $albums[0]); - $artist = $this->getDatabase()->getDocument('artist', 'artist2'); + $artist = static::getDatabase()->getDocument('artist', 'artist2'); $albums = $artist->getAttribute('albums', []); $this->assertEquals('album2', $albums[0]['$id']); $this->assertArrayNotHasKey('artist', $albums[0]); @@ -7585,18 +7625,18 @@ public function testOneToManyOneWayRelationship(): void $this->assertCount(2, $albums); // Get related document - $album = $this->getDatabase()->getDocument('album', 'album1'); + $album = static::getDatabase()->getDocument('album', 'album1'); $this->assertArrayNotHasKey('artist', $album); - $album = $this->getDatabase()->getDocument('album', 'album2'); + $album = static::getDatabase()->getDocument('album', 'album2'); $this->assertArrayNotHasKey('artist', $album); - $artists = $this->getDatabase()->find('artist'); + $artists = static::getDatabase()->find('artist'); $this->assertEquals(2, \count($artists)); // Select related document attributes - $artist = $this->getDatabase()->findOne('artist', [ + $artist = static::getDatabase()->findOne('artist', [ Query::select(['*', 'albums.name']) ]); @@ -7607,7 +7647,7 @@ public function testOneToManyOneWayRelationship(): void $this->assertEquals('Album 1', $artist->getAttribute('albums')[0]->getAttribute('name')); $this->assertArrayNotHasKey('price', $artist->getAttribute('albums')[0]); - $artist = $this->getDatabase()->getDocument('artist', 'artist1', [ + $artist = static::getDatabase()->getDocument('artist', 'artist1', [ Query::select(['*', 'albums.name']) ]); @@ -7615,43 +7655,43 @@ public function testOneToManyOneWayRelationship(): void $this->assertArrayNotHasKey('price', $artist->getAttribute('albums')[0]); // Update root document attribute without altering relationship - $artist1 = $this->getDatabase()->updateDocument( + $artist1 = static::getDatabase()->updateDocument( 'artist', $artist1->getId(), $artist1->setAttribute('name', 'Artist 1 Updated') ); $this->assertEquals('Artist 1 Updated', $artist1->getAttribute('name')); - $artist1 = $this->getDatabase()->getDocument('artist', 'artist1'); + $artist1 = static::getDatabase()->getDocument('artist', 'artist1'); $this->assertEquals('Artist 1 Updated', $artist1->getAttribute('name')); // Update nested document attribute $albums = $artist1->getAttribute('albums', []); $albums[0]->setAttribute('name', 'Album 1 Updated'); - $artist1 = $this->getDatabase()->updateDocument( + $artist1 = static::getDatabase()->updateDocument( 'artist', $artist1->getId(), $artist1->setAttribute('albums', $albums) ); $this->assertEquals('Album 1 Updated', $artist1->getAttribute('albums')[0]->getAttribute('name')); - $artist1 = $this->getDatabase()->getDocument('artist', 'artist1'); + $artist1 = static::getDatabase()->getDocument('artist', 'artist1'); $this->assertEquals('Album 1 Updated', $artist1->getAttribute('albums')[0]->getAttribute('name')); $albumId = $artist1->getAttribute('albums')[0]->getAttribute('$id'); - $albumDocument = $this->getDatabase()->getDocument('album', $albumId); + $albumDocument = static::getDatabase()->getDocument('album', $albumId); $albumDocument->setAttribute('name', 'Album 1 Updated!!!'); - $this->getDatabase()->updateDocument('album', $albumDocument->getId(), $albumDocument); - $albumDocument = $this->getDatabase()->getDocument('album', $albumDocument->getId()); - $artist1 = $this->getDatabase()->getDocument('artist', $artist1->getId()); + static::getDatabase()->updateDocument('album', $albumDocument->getId(), $albumDocument); + $albumDocument = static::getDatabase()->getDocument('album', $albumDocument->getId()); + $artist1 = static::getDatabase()->getDocument('artist', $artist1->getId()); $this->assertEquals('Album 1 Updated!!!', $albumDocument['name']); $this->assertEquals($albumDocument->getId(), $artist1->getAttribute('albums')[0]->getId()); $this->assertEquals($albumDocument->getAttribute('name'), $artist1->getAttribute('albums')[0]->getAttribute('name')); // Create new document with no relationship - $artist3 = $this->getDatabase()->createDocument('artist', new Document([ + $artist3 = static::getDatabase()->createDocument('artist', new Document([ '$id' => 'artist3', '$permissions' => [ Permission::read(Role::any()), @@ -7662,7 +7702,7 @@ public function testOneToManyOneWayRelationship(): void ])); // Update to relate to created document - $artist3 = $this->getDatabase()->updateDocument( + $artist3 = static::getDatabase()->updateDocument( 'artist', $artist3->getId(), $artist3->setAttribute('albums', [new Document([ @@ -7678,37 +7718,37 @@ public function testOneToManyOneWayRelationship(): void ); $this->assertEquals('Album 3', $artist3->getAttribute('albums')[0]->getAttribute('name')); - $artist3 = $this->getDatabase()->getDocument('artist', 'artist3'); + $artist3 = static::getDatabase()->getDocument('artist', 'artist3'); $this->assertEquals('Album 3', $artist3->getAttribute('albums')[0]->getAttribute('name')); // Update document with new related documents, will remove existing relations - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'artist', $artist1->getId(), $artist1->setAttribute('albums', ['album2']) ); // Update document with new related documents, will remove existing relations - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'artist', $artist1->getId(), $artist1->setAttribute('albums', ['album1', 'album2']) ); // Rename relationship key - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'artist', 'albums', 'newAlbums' ); // Get document with new relationship key - $artist = $this->getDatabase()->getDocument('artist', 'artist1'); + $artist = static::getDatabase()->getDocument('artist', 'artist1'); $albums = $artist->getAttribute('newAlbums'); $this->assertEquals('album1', $albums[0]['$id']); // Create new document with no relationship - $this->getDatabase()->createDocument('artist', new Document([ + static::getDatabase()->createDocument('artist', new Document([ '$id' => 'artist4', '$permissions' => [ Permission::read(Role::any()), @@ -7719,56 +7759,56 @@ public function testOneToManyOneWayRelationship(): void ])); // Can delete document with no relationship when on delete is set to restrict - $deleted = $this->getDatabase()->deleteDocument('artist', 'artist4'); + $deleted = static::getDatabase()->deleteDocument('artist', 'artist4'); $this->assertEquals(true, $deleted); - $artist4 = $this->getDatabase()->getDocument('artist', 'artist4'); + $artist4 = static::getDatabase()->getDocument('artist', 'artist4'); $this->assertEquals(true, $artist4->isEmpty()); // Try to delete document while still related to another with on delete: restrict try { - $this->getDatabase()->deleteDocument('artist', 'artist1'); + static::getDatabase()->deleteDocument('artist', 'artist1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'artist', id: 'newAlbums', onDelete: Database::RELATION_MUTATE_SET_NULL ); // Delete parent, set child relationship to null - $this->getDatabase()->deleteDocument('artist', 'artist1'); + static::getDatabase()->deleteDocument('artist', 'artist1'); // Check relation was set to null - $album2 = $this->getDatabase()->getDocument('album', 'album2'); + $album2 = static::getDatabase()->getDocument('album', 'album2'); $this->assertEquals(null, $album2->getAttribute('artist', '')); // Relate again - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'album', $album2->getId(), $album2->setAttribute('artist', 'artist2') ); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'artist', id: 'newAlbums', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete parent, will delete child - $this->getDatabase()->deleteDocument('artist', 'artist2'); + static::getDatabase()->deleteDocument('artist', 'artist2'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('artist', 'artist2'); + $library = static::getDatabase()->getDocument('artist', 'artist2'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('album', 'album2'); + $library = static::getDatabase()->getDocument('album', 'album2'); $this->assertEquals(true, $library->isEmpty()); $albums = []; @@ -7785,7 +7825,7 @@ public function testOneToManyOneWayRelationship(): void ]; } - $artist = $this->getDatabase()->createDocument('artist', new Document([ + $artist = static::getDatabase()->createDocument('artist', new Document([ '$permissions' => [ Permission::read(Role::any()), Permission::delete(Role::any()), @@ -7794,29 +7834,29 @@ public function testOneToManyOneWayRelationship(): void 'newAlbums' => $albums ])); - $artist = $this->getDatabase()->getDocument('artist', $artist->getId()); + $artist = static::getDatabase()->getDocument('artist', $artist->getId()); $this->assertCount(50, $artist->getAttribute('newAlbums')); - $albums = $this->getDatabase()->find('album', [ + $albums = static::getDatabase()->find('album', [ Query::equal('artist', [$artist->getId()]), Query::limit(999) ]); $this->assertCount(50, $albums); - $count = $this->getDatabase()->count('album', [ + $count = static::getDatabase()->count('album', [ Query::equal('artist', [$artist->getId()]), ]); $this->assertEquals(50, $count); - $this->getDatabase()->deleteDocument('album', 'album_1'); - $artist = $this->getDatabase()->getDocument('artist', $artist->getId()); + static::getDatabase()->deleteDocument('album', 'album_1'); + $artist = static::getDatabase()->getDocument('artist', $artist->getId()); $this->assertCount(49, $artist->getAttribute('newAlbums')); - $this->getDatabase()->deleteDocument('artist', $artist->getId()); + static::getDatabase()->deleteDocument('artist', $artist->getId()); - $albums = $this->getDatabase()->find('album', [ + $albums = static::getDatabase()->find('album', [ Query::equal('artist', [$artist->getId()]), Query::limit(999) ]); @@ -7824,32 +7864,32 @@ public function testOneToManyOneWayRelationship(): void $this->assertCount(0, $albums); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'artist', 'newAlbums' ); // Try to get document again - $artist = $this->getDatabase()->getDocument('artist', 'artist1'); + $artist = static::getDatabase()->getDocument('artist', 'artist1'); $albums = $artist->getAttribute('newAlbums', ''); $this->assertEquals(null, $albums); } public function testOneToManyTwoWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('customer'); - $this->getDatabase()->createCollection('account'); + static::getDatabase()->createCollection('customer'); + static::getDatabase()->createCollection('account'); - $this->getDatabase()->createAttribute('customer', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('account', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('account', 'number', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('customer', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('account', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('account', 'number', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'customer', relatedCollection: 'account', type: Database::RELATION_ONE_TO_MANY, @@ -7858,7 +7898,7 @@ public function testOneToManyTwoWayRelationship(): void ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('customer'); + $collection = static::getDatabase()->getCollection('customer'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'accounts') { @@ -7873,7 +7913,7 @@ public function testOneToManyTwoWayRelationship(): void } // Check metadata for related collection - $collection = $this->getDatabase()->getCollection('account'); + $collection = static::getDatabase()->getCollection('account'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'customer') { @@ -7888,7 +7928,7 @@ public function testOneToManyTwoWayRelationship(): void } // Create document with relationship with nested data - $customer1 = $this->getDatabase()->createDocument('customer', new Document([ + $customer1 = static::getDatabase()->createDocument('customer', new Document([ '$id' => 'customer1', '$permissions' => [ Permission::read(Role::any()), @@ -7911,14 +7951,14 @@ public function testOneToManyTwoWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('customer', 'customer1', $customer1->setAttribute('accounts', ['account1','no-account'])); + static::getDatabase()->updateDocument('customer', 'customer1', $customer1->setAttribute('accounts', ['account1','no-account'])); - $customer1Document = $this->getDatabase()->getDocument('customer', 'customer1'); + $customer1Document = static::getDatabase()->getDocument('customer', 'customer1'); // Assert document does not contain non existing relation document. $this->assertEquals(1, \count($customer1Document->getAttribute('accounts'))); // Create document with relationship with related ID - $account2 = $this->getDatabase()->createDocument('account', new Document([ + $account2 = static::getDatabase()->createDocument('account', new Document([ '$id' => 'account2', '$permissions' => [ Permission::read(Role::any()), @@ -7928,7 +7968,7 @@ public function testOneToManyTwoWayRelationship(): void 'name' => 'Account 2', 'number' => '987654321', ])); - $this->getDatabase()->createDocument('customer', new Document([ + static::getDatabase()->createDocument('customer', new Document([ '$id' => 'customer2', '$permissions' => [ Permission::read(Role::any()), @@ -7942,7 +7982,7 @@ public function testOneToManyTwoWayRelationship(): void ])); // Create from child side - $this->getDatabase()->createDocument('account', new Document([ + static::getDatabase()->createDocument('account', new Document([ '$id' => 'account3', '$permissions' => [ Permission::read(Role::any()), @@ -7961,7 +8001,7 @@ public function testOneToManyTwoWayRelationship(): void 'name' => 'Customer 3' ] ])); - $this->getDatabase()->createDocument('customer', new Document([ + static::getDatabase()->createDocument('customer', new Document([ '$id' => 'customer4', '$permissions' => [ Permission::read(Role::any()), @@ -7970,7 +8010,7 @@ public function testOneToManyTwoWayRelationship(): void ], 'name' => 'Customer 4', ])); - $this->getDatabase()->createDocument('account', new Document([ + static::getDatabase()->createDocument('account', new Document([ '$id' => 'account4', '$permissions' => [ Permission::read(Role::any()), @@ -7983,53 +8023,53 @@ public function testOneToManyTwoWayRelationship(): void ])); // Get documents with relationship - $customer = $this->getDatabase()->getDocument('customer', 'customer1'); + $customer = static::getDatabase()->getDocument('customer', 'customer1'); $accounts = $customer->getAttribute('accounts', []); $this->assertEquals('account1', $accounts[0]['$id']); $this->assertArrayNotHasKey('customer', $accounts[0]); - $customer = $this->getDatabase()->getDocument('customer', 'customer2'); + $customer = static::getDatabase()->getDocument('customer', 'customer2'); $accounts = $customer->getAttribute('accounts', []); $this->assertEquals('account2', $accounts[0]['$id']); $this->assertArrayNotHasKey('customer', $accounts[0]); - $customer = $this->getDatabase()->getDocument('customer', 'customer3'); + $customer = static::getDatabase()->getDocument('customer', 'customer3'); $accounts = $customer->getAttribute('accounts', []); $this->assertEquals('account3', $accounts[0]['$id']); $this->assertArrayNotHasKey('customer', $accounts[0]); - $customer = $this->getDatabase()->getDocument('customer', 'customer4'); + $customer = static::getDatabase()->getDocument('customer', 'customer4'); $accounts = $customer->getAttribute('accounts', []); $this->assertEquals('account4', $accounts[0]['$id']); $this->assertArrayNotHasKey('customer', $accounts[0]); // Get related documents - $account = $this->getDatabase()->getDocument('account', 'account1'); + $account = static::getDatabase()->getDocument('account', 'account1'); $customer = $account->getAttribute('customer'); $this->assertEquals('customer1', $customer['$id']); $this->assertArrayNotHasKey('accounts', $customer); - $account = $this->getDatabase()->getDocument('account', 'account2'); + $account = static::getDatabase()->getDocument('account', 'account2'); $customer = $account->getAttribute('customer'); $this->assertEquals('customer2', $customer['$id']); $this->assertArrayNotHasKey('accounts', $customer); - $account = $this->getDatabase()->getDocument('account', 'account3'); + $account = static::getDatabase()->getDocument('account', 'account3'); $customer = $account->getAttribute('customer'); $this->assertEquals('customer3', $customer['$id']); $this->assertArrayNotHasKey('accounts', $customer); - $account = $this->getDatabase()->getDocument('account', 'account4'); + $account = static::getDatabase()->getDocument('account', 'account4'); $customer = $account->getAttribute('customer'); $this->assertEquals('customer4', $customer['$id']); $this->assertArrayNotHasKey('accounts', $customer); - $customers = $this->getDatabase()->find('customer'); + $customers = static::getDatabase()->find('customer'); $this->assertEquals(4, \count($customers)); // Select related document attributes - $customer = $this->getDatabase()->findOne('customer', [ + $customer = static::getDatabase()->findOne('customer', [ Query::select(['*', 'accounts.name']) ]); @@ -8040,7 +8080,7 @@ public function testOneToManyTwoWayRelationship(): void $this->assertEquals('Account 1', $customer->getAttribute('accounts')[0]->getAttribute('name')); $this->assertArrayNotHasKey('number', $customer->getAttribute('accounts')[0]); - $customer = $this->getDatabase()->getDocument('customer', 'customer1', [ + $customer = static::getDatabase()->getDocument('customer', 'customer1', [ Query::select(['*', 'accounts.name']) ]); @@ -8048,45 +8088,45 @@ public function testOneToManyTwoWayRelationship(): void $this->assertArrayNotHasKey('number', $customer->getAttribute('accounts')[0]); // Update root document attribute without altering relationship - $customer1 = $this->getDatabase()->updateDocument( + $customer1 = static::getDatabase()->updateDocument( 'customer', $customer1->getId(), $customer1->setAttribute('name', 'Customer 1 Updated') ); $this->assertEquals('Customer 1 Updated', $customer1->getAttribute('name')); - $customer1 = $this->getDatabase()->getDocument('customer', 'customer1'); + $customer1 = static::getDatabase()->getDocument('customer', 'customer1'); $this->assertEquals('Customer 1 Updated', $customer1->getAttribute('name')); - $account2 = $this->getDatabase()->getDocument('account', 'account2'); + $account2 = static::getDatabase()->getDocument('account', 'account2'); // Update inverse root document attribute without altering relationship - $account2 = $this->getDatabase()->updateDocument( + $account2 = static::getDatabase()->updateDocument( 'account', $account2->getId(), $account2->setAttribute('name', 'Account 2 Updated') ); $this->assertEquals('Account 2 Updated', $account2->getAttribute('name')); - $account2 = $this->getDatabase()->getDocument('account', 'account2'); + $account2 = static::getDatabase()->getDocument('account', 'account2'); $this->assertEquals('Account 2 Updated', $account2->getAttribute('name')); // Update nested document attribute $accounts = $customer1->getAttribute('accounts', []); $accounts[0]->setAttribute('name', 'Account 1 Updated'); - $customer1 = $this->getDatabase()->updateDocument( + $customer1 = static::getDatabase()->updateDocument( 'customer', $customer1->getId(), $customer1->setAttribute('accounts', $accounts) ); $this->assertEquals('Account 1 Updated', $customer1->getAttribute('accounts')[0]->getAttribute('name')); - $customer1 = $this->getDatabase()->getDocument('customer', 'customer1'); + $customer1 = static::getDatabase()->getDocument('customer', 'customer1'); $this->assertEquals('Account 1 Updated', $customer1->getAttribute('accounts')[0]->getAttribute('name')); // Update inverse nested document attribute - $account2 = $this->getDatabase()->updateDocument( + $account2 = static::getDatabase()->updateDocument( 'account', $account2->getId(), $account2->setAttribute( @@ -8098,11 +8138,11 @@ public function testOneToManyTwoWayRelationship(): void ); $this->assertEquals('Customer 2 Updated', $account2->getAttribute('customer')->getAttribute('name')); - $account2 = $this->getDatabase()->getDocument('account', 'account2'); + $account2 = static::getDatabase()->getDocument('account', 'account2'); $this->assertEquals('Customer 2 Updated', $account2->getAttribute('customer')->getAttribute('name')); // Create new document with no relationship - $customer5 = $this->getDatabase()->createDocument('customer', new Document([ + $customer5 = static::getDatabase()->createDocument('customer', new Document([ '$id' => 'customer5', '$permissions' => [ Permission::read(Role::any()), @@ -8113,7 +8153,7 @@ public function testOneToManyTwoWayRelationship(): void ])); // Update to relate to created document - $customer5 = $this->getDatabase()->updateDocument( + $customer5 = static::getDatabase()->updateDocument( 'customer', $customer5->getId(), $customer5->setAttribute('accounts', [new Document([ @@ -8129,11 +8169,11 @@ public function testOneToManyTwoWayRelationship(): void ); $this->assertEquals('Account 5', $customer5->getAttribute('accounts')[0]->getAttribute('name')); - $customer5 = $this->getDatabase()->getDocument('customer', 'customer5'); + $customer5 = static::getDatabase()->getDocument('customer', 'customer5'); $this->assertEquals('Account 5', $customer5->getAttribute('accounts')[0]->getAttribute('name')); // Create new child document with no relationship - $account6 = $this->getDatabase()->createDocument('account', new Document([ + $account6 = static::getDatabase()->createDocument('account', new Document([ '$id' => 'account6', '$permissions' => [ Permission::read(Role::any()), @@ -8145,7 +8185,7 @@ public function testOneToManyTwoWayRelationship(): void ])); // Update inverse to relate to created document - $account6 = $this->getDatabase()->updateDocument( + $account6 = static::getDatabase()->updateDocument( 'account', $account6->getId(), $account6->setAttribute('customer', new Document([ @@ -8160,32 +8200,32 @@ public function testOneToManyTwoWayRelationship(): void ); $this->assertEquals('Customer 6', $account6->getAttribute('customer')->getAttribute('name')); - $account6 = $this->getDatabase()->getDocument('account', 'account6'); + $account6 = static::getDatabase()->getDocument('account', 'account6'); $this->assertEquals('Customer 6', $account6->getAttribute('customer')->getAttribute('name')); // Update document with new related document, will remove existing relations - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'customer', $customer1->getId(), $customer1->setAttribute('accounts', ['account2']) ); // Update document with new related document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'customer', $customer1->getId(), $customer1->setAttribute('accounts', ['account1', 'account2']) ); // Update inverse document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'account', $account2->getId(), $account2->setAttribute('customer', 'customer2') ); // Rename relationship keys on both sides - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'customer', 'accounts', 'newAccounts', @@ -8193,17 +8233,17 @@ public function testOneToManyTwoWayRelationship(): void ); // Get document with new relationship key - $customer = $this->getDatabase()->getDocument('customer', 'customer1'); + $customer = static::getDatabase()->getDocument('customer', 'customer1'); $accounts = $customer->getAttribute('newAccounts'); $this->assertEquals('account1', $accounts[0]['$id']); // Get inverse document with new relationship key - $account = $this->getDatabase()->getDocument('account', 'account1'); + $account = static::getDatabase()->getDocument('account', 'account1'); $customer = $account->getAttribute('newCustomer'); $this->assertEquals('customer1', $customer['$id']); // Create new document with no relationship - $this->getDatabase()->createDocument('customer', new Document([ + static::getDatabase()->createDocument('customer', new Document([ '$id' => 'customer7', '$permissions' => [ Permission::read(Role::any()), @@ -8214,91 +8254,91 @@ public function testOneToManyTwoWayRelationship(): void ])); // Can delete document with no relationship when on delete is set to restrict - $deleted = $this->getDatabase()->deleteDocument('customer', 'customer7'); + $deleted = static::getDatabase()->deleteDocument('customer', 'customer7'); $this->assertEquals(true, $deleted); - $customer7 = $this->getDatabase()->getDocument('customer', 'customer7'); + $customer7 = static::getDatabase()->getDocument('customer', 'customer7'); $this->assertEquals(true, $customer7->isEmpty()); // Try to delete document while still related to another with on delete: restrict try { - $this->getDatabase()->deleteDocument('customer', 'customer1'); + static::getDatabase()->deleteDocument('customer', 'customer1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'customer', id: 'newAccounts', onDelete: Database::RELATION_MUTATE_SET_NULL ); // Delete parent, set child relationship to null - $this->getDatabase()->deleteDocument('customer', 'customer1'); + static::getDatabase()->deleteDocument('customer', 'customer1'); // Check relation was set to null - $account1 = $this->getDatabase()->getDocument('account', 'account1'); + $account1 = static::getDatabase()->getDocument('account', 'account1'); $this->assertEquals(null, $account2->getAttribute('newCustomer', '')); // Relate again - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'account', $account1->getId(), $account1->setAttribute('newCustomer', 'customer2') ); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'customer', id: 'newAccounts', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete parent, will delete child - $this->getDatabase()->deleteDocument('customer', 'customer2'); + static::getDatabase()->deleteDocument('customer', 'customer2'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('customer', 'customer2'); + $library = static::getDatabase()->getDocument('customer', 'customer2'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('account', 'account2'); + $library = static::getDatabase()->getDocument('account', 'account2'); $this->assertEquals(true, $library->isEmpty()); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'customer', 'newAccounts' ); // Try to get document again - $customer = $this->getDatabase()->getDocument('customer', 'customer1'); + $customer = static::getDatabase()->getDocument('customer', 'customer1'); $accounts = $customer->getAttribute('newAccounts'); $this->assertEquals(null, $accounts); // Try to get inverse document again - $accounts = $this->getDatabase()->getDocument('account', 'account1'); + $accounts = static::getDatabase()->getDocument('account', 'account1'); $customer = $accounts->getAttribute('newCustomer'); $this->assertEquals(null, $customer); } public function testManyToOneOneWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('review'); - $this->getDatabase()->createCollection('movie'); + static::getDatabase()->createCollection('review'); + static::getDatabase()->createCollection('movie'); - $this->getDatabase()->createAttribute('review', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('movie', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('movie', 'length', Database::VAR_INTEGER, 0, true, formatOptions: ['min' => 0, 'max' => 999]); - $this->getDatabase()->createAttribute('movie', 'date', Database::VAR_DATETIME, 0, false, filters: ['datetime']); - $this->getDatabase()->createAttribute('review', 'date', Database::VAR_DATETIME, 0, false, filters: ['datetime']); - $this->getDatabase()->createRelationship( + static::getDatabase()->createAttribute('review', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('movie', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('movie', 'length', Database::VAR_INTEGER, 0, true, formatOptions: ['min' => 0, 'max' => 999]); + static::getDatabase()->createAttribute('movie', 'date', Database::VAR_DATETIME, 0, false, filters: ['datetime']); + static::getDatabase()->createAttribute('review', 'date', Database::VAR_DATETIME, 0, false, filters: ['datetime']); + static::getDatabase()->createRelationship( collection: 'review', relatedCollection: 'movie', type: Database::RELATION_MANY_TO_ONE, @@ -8306,7 +8346,7 @@ public function testManyToOneOneWayRelationship(): void ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('review'); + $collection = static::getDatabase()->getCollection('review'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'movie') { @@ -8321,7 +8361,7 @@ public function testManyToOneOneWayRelationship(): void } // Check metadata for related collection - $collection = $this->getDatabase()->getCollection('movie'); + $collection = static::getDatabase()->getCollection('movie'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'reviews') { @@ -8336,7 +8376,7 @@ public function testManyToOneOneWayRelationship(): void } // Create document with relationship with nested data - $review1 = $this->getDatabase()->createDocument('review', new Document([ + $review1 = static::getDatabase()->createDocument('review', new Document([ '$id' => 'review1', '$permissions' => [ Permission::read(Role::any()), @@ -8359,16 +8399,16 @@ public function testManyToOneOneWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('review', 'review1', $review1->setAttribute('movie', 'no-movie')); + static::getDatabase()->updateDocument('review', 'review1', $review1->setAttribute('movie', 'no-movie')); - $review1Document = $this->getDatabase()->getDocument('review', 'review1'); + $review1Document = static::getDatabase()->getDocument('review', 'review1'); // Assert document does not contain non existing relation document. $this->assertEquals(null, $review1Document->getAttribute('movie')); - $this->getDatabase()->updateDocument('review', 'review1', $review1->setAttribute('movie', 'movie1')); + static::getDatabase()->updateDocument('review', 'review1', $review1->setAttribute('movie', 'movie1')); // Create document with relationship to existing document by ID - $review10 = $this->getDatabase()->createDocument('review', new Document([ + $review10 = static::getDatabase()->createDocument('review', new Document([ '$id' => 'review10', '$permissions' => [ Permission::read(Role::any()), @@ -8381,7 +8421,7 @@ public function testManyToOneOneWayRelationship(): void ])); // Create document with relationship with related ID - $this->getDatabase()->createDocument('movie', new Document([ + static::getDatabase()->createDocument('movie', new Document([ '$id' => 'movie2', '$permissions' => [ Permission::read(Role::any()), @@ -8392,7 +8432,7 @@ public function testManyToOneOneWayRelationship(): void 'length' => 90, 'date' => '2023-04-03 10:35:27.390', ])); - $this->getDatabase()->createDocument('review', new Document([ + static::getDatabase()->createDocument('review', new Document([ '$id' => 'review2', '$permissions' => [ Permission::read(Role::any()), @@ -8405,12 +8445,12 @@ public function testManyToOneOneWayRelationship(): void ])); // Get document with relationship - $review = $this->getDatabase()->getDocument('review', 'review1'); + $review = static::getDatabase()->getDocument('review', 'review1'); $movie = $review->getAttribute('movie', []); $this->assertEquals('movie1', $movie['$id']); $this->assertArrayNotHasKey('reviews', $movie); - $documents = $this->getDatabase()->find('review', [ + $documents = static::getDatabase()->find('review', [ Query::select(['date', 'movie.date']) ]); @@ -8424,24 +8464,24 @@ public function testManyToOneOneWayRelationship(): void $this->assertEquals(29, strlen($document['date'])); // checks filter $this->assertEquals(29, strlen($document['movie']['date'])); - $review = $this->getDatabase()->getDocument('review', 'review2'); + $review = static::getDatabase()->getDocument('review', 'review2'); $movie = $review->getAttribute('movie', []); $this->assertEquals('movie2', $movie['$id']); $this->assertArrayNotHasKey('reviews', $movie); // Get related document - $movie = $this->getDatabase()->getDocument('movie', 'movie1'); + $movie = static::getDatabase()->getDocument('movie', 'movie1'); $this->assertArrayNotHasKey('reviews', $movie); - $movie = $this->getDatabase()->getDocument('movie', 'movie2'); + $movie = static::getDatabase()->getDocument('movie', 'movie2'); $this->assertArrayNotHasKey('reviews', $movie); - $reviews = $this->getDatabase()->find('review'); + $reviews = static::getDatabase()->find('review'); $this->assertEquals(3, \count($reviews)); // Select related document attributes - $review = $this->getDatabase()->findOne('review', [ + $review = static::getDatabase()->findOne('review', [ Query::select(['*', 'movie.name']) ]); @@ -8452,7 +8492,7 @@ public function testManyToOneOneWayRelationship(): void $this->assertEquals('Movie 1', $review->getAttribute('movie')->getAttribute('name')); $this->assertArrayNotHasKey('length', $review->getAttribute('movie')); - $review = $this->getDatabase()->getDocument('review', 'review1', [ + $review = static::getDatabase()->getDocument('review', 'review1', [ Query::select(['*', 'movie.name']) ]); @@ -8460,32 +8500,32 @@ public function testManyToOneOneWayRelationship(): void $this->assertArrayNotHasKey('length', $review->getAttribute('movie')); // Update root document attribute without altering relationship - $review1 = $this->getDatabase()->updateDocument( + $review1 = static::getDatabase()->updateDocument( 'review', $review1->getId(), $review1->setAttribute('name', 'Review 1 Updated') ); $this->assertEquals('Review 1 Updated', $review1->getAttribute('name')); - $review1 = $this->getDatabase()->getDocument('review', 'review1'); + $review1 = static::getDatabase()->getDocument('review', 'review1'); $this->assertEquals('Review 1 Updated', $review1->getAttribute('name')); // Update nested document attribute $movie = $review1->getAttribute('movie'); $movie->setAttribute('name', 'Movie 1 Updated'); - $review1 = $this->getDatabase()->updateDocument( + $review1 = static::getDatabase()->updateDocument( 'review', $review1->getId(), $review1->setAttribute('movie', $movie) ); $this->assertEquals('Movie 1 Updated', $review1->getAttribute('movie')->getAttribute('name')); - $review1 = $this->getDatabase()->getDocument('review', 'review1'); + $review1 = static::getDatabase()->getDocument('review', 'review1'); $this->assertEquals('Movie 1 Updated', $review1->getAttribute('movie')->getAttribute('name')); // Create new document with no relationship - $review5 = $this->getDatabase()->createDocument('review', new Document([ + $review5 = static::getDatabase()->createDocument('review', new Document([ '$id' => 'review5', '$permissions' => [ Permission::read(Role::any()), @@ -8496,7 +8536,7 @@ public function testManyToOneOneWayRelationship(): void ])); // Update to relate to created document - $review5 = $this->getDatabase()->updateDocument( + $review5 = static::getDatabase()->updateDocument( 'review', $review5->getId(), $review5->setAttribute('movie', new Document([ @@ -8512,39 +8552,39 @@ public function testManyToOneOneWayRelationship(): void ); $this->assertEquals('Movie 5', $review5->getAttribute('movie')->getAttribute('name')); - $review5 = $this->getDatabase()->getDocument('review', 'review5'); + $review5 = static::getDatabase()->getDocument('review', 'review5'); $this->assertEquals('Movie 5', $review5->getAttribute('movie')->getAttribute('name')); // Update document with new related document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'review', $review1->getId(), $review1->setAttribute('movie', 'movie2') ); // Rename relationship keys on both sides - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'review', 'movie', 'newMovie', ); // Get document with new relationship key - $review = $this->getDatabase()->getDocument('review', 'review1'); + $review = static::getDatabase()->getDocument('review', 'review1'); $movie = $review->getAttribute('newMovie'); $this->assertEquals('movie2', $movie['$id']); // Reset values - $review1 = $this->getDatabase()->getDocument('review', 'review1'); + $review1 = static::getDatabase()->getDocument('review', 'review1'); - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'review', $review1->getId(), $review1->setAttribute('newMovie', 'movie1') ); // Create new document with no relationship - $this->getDatabase()->createDocument('movie', new Document([ + static::getDatabase()->createDocument('movie', new Document([ '$id' => 'movie3', '$permissions' => [ Permission::read(Role::any()), @@ -8556,78 +8596,78 @@ public function testManyToOneOneWayRelationship(): void ])); // Can delete document with no relationship when on delete is set to restrict - $deleted = $this->getDatabase()->deleteDocument('movie', 'movie3'); + $deleted = static::getDatabase()->deleteDocument('movie', 'movie3'); $this->assertEquals(true, $deleted); - $movie3 = $this->getDatabase()->getDocument('movie', 'movie3'); + $movie3 = static::getDatabase()->getDocument('movie', 'movie3'); $this->assertEquals(true, $movie3->isEmpty()); // Try to delete document while still related to another with on delete: restrict try { - $this->getDatabase()->deleteDocument('movie', 'movie1'); + static::getDatabase()->deleteDocument('movie', 'movie1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'review', id: 'newMovie', onDelete: Database::RELATION_MUTATE_SET_NULL ); // Delete child, set parent relationship to null - $this->getDatabase()->deleteDocument('movie', 'movie1'); + static::getDatabase()->deleteDocument('movie', 'movie1'); // Check relation was set to null - $review1 = $this->getDatabase()->getDocument('review', 'review1'); + $review1 = static::getDatabase()->getDocument('review', 'review1'); $this->assertEquals(null, $review1->getAttribute('newMovie')); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'review', id: 'newMovie', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete child, will delete parent - $this->getDatabase()->deleteDocument('movie', 'movie2'); + static::getDatabase()->deleteDocument('movie', 'movie2'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('movie', 'movie2'); + $library = static::getDatabase()->getDocument('movie', 'movie2'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('review', 'review2'); + $library = static::getDatabase()->getDocument('review', 'review2'); $this->assertEquals(true, $library->isEmpty()); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'review', 'newMovie' ); // Try to get document again - $review = $this->getDatabase()->getDocument('review', 'review1'); + $review = static::getDatabase()->getDocument('review', 'review1'); $movie = $review->getAttribute('newMovie'); $this->assertEquals(null, $movie); } public function testManyToOneTwoWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('product'); - $this->getDatabase()->createCollection('store'); + static::getDatabase()->createCollection('product'); + static::getDatabase()->createCollection('store'); - $this->getDatabase()->createAttribute('store', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('store', 'opensAt', Database::VAR_STRING, 5, true); + static::getDatabase()->createAttribute('store', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('store', 'opensAt', Database::VAR_STRING, 5, true); - $this->getDatabase()->createAttribute( + static::getDatabase()->createAttribute( collection: 'product', id: 'name', type: Database::VAR_STRING, @@ -8635,7 +8675,7 @@ public function testManyToOneTwoWayRelationship(): void required: true ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'product', relatedCollection: 'store', type: Database::RELATION_MANY_TO_ONE, @@ -8644,7 +8684,7 @@ public function testManyToOneTwoWayRelationship(): void ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('product'); + $collection = static::getDatabase()->getCollection('product'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'store') { @@ -8659,7 +8699,7 @@ public function testManyToOneTwoWayRelationship(): void } // Check metadata for related collection - $collection = $this->getDatabase()->getCollection('store'); + $collection = static::getDatabase()->getCollection('store'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'products') { @@ -8674,7 +8714,7 @@ public function testManyToOneTwoWayRelationship(): void } // Create document with relationship with nested data - $product1 = $this->getDatabase()->createDocument('product', new Document([ + $product1 = static::getDatabase()->createDocument('product', new Document([ '$id' => 'product1', '$permissions' => [ Permission::read(Role::any()), @@ -8695,16 +8735,16 @@ public function testManyToOneTwoWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('product', 'product1', $product1->setAttribute('store', 'no-store')); + static::getDatabase()->updateDocument('product', 'product1', $product1->setAttribute('store', 'no-store')); - $product1Document = $this->getDatabase()->getDocument('product', 'product1'); + $product1Document = static::getDatabase()->getDocument('product', 'product1'); // Assert document does not contain non existing relation document. $this->assertEquals(null, $product1Document->getAttribute('store')); - $this->getDatabase()->updateDocument('product', 'product1', $product1->setAttribute('store', 'store1')); + static::getDatabase()->updateDocument('product', 'product1', $product1->setAttribute('store', 'store1')); // Create document with relationship with related ID - $this->getDatabase()->createDocument('store', new Document([ + static::getDatabase()->createDocument('store', new Document([ '$id' => 'store2', '$permissions' => [ Permission::read(Role::any()), @@ -8714,7 +8754,7 @@ public function testManyToOneTwoWayRelationship(): void 'name' => 'Store 2', 'opensAt' => '09:30', ])); - $this->getDatabase()->createDocument('product', new Document([ + static::getDatabase()->createDocument('product', new Document([ '$id' => 'product2', '$permissions' => [ Permission::read(Role::any()), @@ -8726,7 +8766,7 @@ public function testManyToOneTwoWayRelationship(): void ])); // Create from child side - $this->getDatabase()->createDocument('store', new Document([ + static::getDatabase()->createDocument('store', new Document([ '$id' => 'store3', '$permissions' => [ Permission::read(Role::any()), @@ -8748,7 +8788,7 @@ public function testManyToOneTwoWayRelationship(): void ], ])); - $this->getDatabase()->createDocument('product', new Document([ + static::getDatabase()->createDocument('product', new Document([ '$id' => 'product4', '$permissions' => [ Permission::read(Role::any()), @@ -8757,7 +8797,7 @@ public function testManyToOneTwoWayRelationship(): void ], 'name' => 'Product 4', ])); - $this->getDatabase()->createDocument('store', new Document([ + static::getDatabase()->createDocument('store', new Document([ '$id' => 'store4', '$permissions' => [ Permission::read(Role::any()), @@ -8772,53 +8812,53 @@ public function testManyToOneTwoWayRelationship(): void ])); // Get document with relationship - $product = $this->getDatabase()->getDocument('product', 'product1'); + $product = static::getDatabase()->getDocument('product', 'product1'); $store = $product->getAttribute('store', []); $this->assertEquals('store1', $store['$id']); $this->assertArrayNotHasKey('products', $store); - $product = $this->getDatabase()->getDocument('product', 'product2'); + $product = static::getDatabase()->getDocument('product', 'product2'); $store = $product->getAttribute('store', []); $this->assertEquals('store2', $store['$id']); $this->assertArrayNotHasKey('products', $store); - $product = $this->getDatabase()->getDocument('product', 'product3'); + $product = static::getDatabase()->getDocument('product', 'product3'); $store = $product->getAttribute('store', []); $this->assertEquals('store3', $store['$id']); $this->assertArrayNotHasKey('products', $store); - $product = $this->getDatabase()->getDocument('product', 'product4'); + $product = static::getDatabase()->getDocument('product', 'product4'); $store = $product->getAttribute('store', []); $this->assertEquals('store4', $store['$id']); $this->assertArrayNotHasKey('products', $store); // Get related document - $store = $this->getDatabase()->getDocument('store', 'store1'); + $store = static::getDatabase()->getDocument('store', 'store1'); $products = $store->getAttribute('products'); $this->assertEquals('product1', $products[0]['$id']); $this->assertArrayNotHasKey('store', $products[0]); - $store = $this->getDatabase()->getDocument('store', 'store2'); + $store = static::getDatabase()->getDocument('store', 'store2'); $products = $store->getAttribute('products'); $this->assertEquals('product2', $products[0]['$id']); $this->assertArrayNotHasKey('store', $products[0]); - $store = $this->getDatabase()->getDocument('store', 'store3'); + $store = static::getDatabase()->getDocument('store', 'store3'); $products = $store->getAttribute('products'); $this->assertEquals('product3', $products[0]['$id']); $this->assertArrayNotHasKey('store', $products[0]); - $store = $this->getDatabase()->getDocument('store', 'store4'); + $store = static::getDatabase()->getDocument('store', 'store4'); $products = $store->getAttribute('products'); $this->assertEquals('product4', $products[0]['$id']); $this->assertArrayNotHasKey('store', $products[0]); - $products = $this->getDatabase()->find('product'); + $products = static::getDatabase()->find('product'); $this->assertEquals(4, \count($products)); // Select related document attributes - $product = $this->getDatabase()->findOne('product', [ + $product = static::getDatabase()->findOne('product', [ Query::select(['*', 'store.name']) ]); @@ -8829,7 +8869,7 @@ public function testManyToOneTwoWayRelationship(): void $this->assertEquals('Store 1', $product->getAttribute('store')->getAttribute('name')); $this->assertArrayNotHasKey('opensAt', $product->getAttribute('store')); - $product = $this->getDatabase()->getDocument('product', 'product1', [ + $product = static::getDatabase()->getDocument('product', 'product1', [ Query::select(['*', 'store.name']) ]); @@ -8837,58 +8877,58 @@ public function testManyToOneTwoWayRelationship(): void $this->assertArrayNotHasKey('opensAt', $product->getAttribute('store')); // Update root document attribute without altering relationship - $product1 = $this->getDatabase()->updateDocument( + $product1 = static::getDatabase()->updateDocument( 'product', $product1->getId(), $product1->setAttribute('name', 'Product 1 Updated') ); $this->assertEquals('Product 1 Updated', $product1->getAttribute('name')); - $product1 = $this->getDatabase()->getDocument('product', 'product1'); + $product1 = static::getDatabase()->getDocument('product', 'product1'); $this->assertEquals('Product 1 Updated', $product1->getAttribute('name')); // Update inverse document attribute without altering relationship - $store1 = $this->getDatabase()->getDocument('store', 'store1'); - $store1 = $this->getDatabase()->updateDocument( + $store1 = static::getDatabase()->getDocument('store', 'store1'); + $store1 = static::getDatabase()->updateDocument( 'store', $store1->getId(), $store1->setAttribute('name', 'Store 1 Updated') ); $this->assertEquals('Store 1 Updated', $store1->getAttribute('name')); - $store1 = $this->getDatabase()->getDocument('store', 'store1'); + $store1 = static::getDatabase()->getDocument('store', 'store1'); $this->assertEquals('Store 1 Updated', $store1->getAttribute('name')); // Update nested document attribute $store = $product1->getAttribute('store'); $store->setAttribute('name', 'Store 1 Updated'); - $product1 = $this->getDatabase()->updateDocument( + $product1 = static::getDatabase()->updateDocument( 'product', $product1->getId(), $product1->setAttribute('store', $store) ); $this->assertEquals('Store 1 Updated', $product1->getAttribute('store')->getAttribute('name')); - $product1 = $this->getDatabase()->getDocument('product', 'product1'); + $product1 = static::getDatabase()->getDocument('product', 'product1'); $this->assertEquals('Store 1 Updated', $product1->getAttribute('store')->getAttribute('name')); // Update inverse nested document attribute $product = $store1->getAttribute('products')[0]; $product->setAttribute('name', 'Product 1 Updated'); - $store1 = $this->getDatabase()->updateDocument( + $store1 = static::getDatabase()->updateDocument( 'store', $store1->getId(), $store1->setAttribute('products', [$product]) ); $this->assertEquals('Product 1 Updated', $store1->getAttribute('products')[0]->getAttribute('name')); - $store1 = $this->getDatabase()->getDocument('store', 'store1'); + $store1 = static::getDatabase()->getDocument('store', 'store1'); $this->assertEquals('Product 1 Updated', $store1->getAttribute('products')[0]->getAttribute('name')); // Create new document with no relationship - $product5 = $this->getDatabase()->createDocument('product', new Document([ + $product5 = static::getDatabase()->createDocument('product', new Document([ '$id' => 'product5', '$permissions' => [ Permission::read(Role::any()), @@ -8899,7 +8939,7 @@ public function testManyToOneTwoWayRelationship(): void ])); // Update to relate to created document - $product5 = $this->getDatabase()->updateDocument( + $product5 = static::getDatabase()->updateDocument( 'product', $product5->getId(), $product5->setAttribute('store', new Document([ @@ -8915,11 +8955,11 @@ public function testManyToOneTwoWayRelationship(): void ); $this->assertEquals('Store 5', $product5->getAttribute('store')->getAttribute('name')); - $product5 = $this->getDatabase()->getDocument('product', 'product5'); + $product5 = static::getDatabase()->getDocument('product', 'product5'); $this->assertEquals('Store 5', $product5->getAttribute('store')->getAttribute('name')); // Create new child document with no relationship - $store6 = $this->getDatabase()->createDocument('store', new Document([ + $store6 = static::getDatabase()->createDocument('store', new Document([ '$id' => 'store6', '$permissions' => [ Permission::read(Role::any()), @@ -8931,7 +8971,7 @@ public function testManyToOneTwoWayRelationship(): void ])); // Update inverse to related to newly created document - $store6 = $this->getDatabase()->updateDocument( + $store6 = static::getDatabase()->updateDocument( 'store', $store6->getId(), $store6->setAttribute('products', [new Document([ @@ -8946,36 +8986,36 @@ public function testManyToOneTwoWayRelationship(): void ); $this->assertEquals('Product 6', $store6->getAttribute('products')[0]->getAttribute('name')); - $store6 = $this->getDatabase()->getDocument('store', 'store6'); + $store6 = static::getDatabase()->getDocument('store', 'store6'); $this->assertEquals('Product 6', $store6->getAttribute('products')[0]->getAttribute('name')); // Update document with new related document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'product', $product1->getId(), $product1->setAttribute('store', 'store2') ); - $store1 = $this->getDatabase()->getDocument('store', 'store1'); + $store1 = static::getDatabase()->getDocument('store', 'store1'); // Update inverse document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'store', $store1->getId(), $store1->setAttribute('products', ['product1']) ); - $store2 = $this->getDatabase()->getDocument('store', 'store2'); + $store2 = static::getDatabase()->getDocument('store', 'store2'); // Update inverse document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'store', $store2->getId(), $store2->setAttribute('products', ['product1', 'product2']) ); // Rename relationship keys on both sides - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'product', 'store', 'newStore', @@ -8983,25 +9023,25 @@ public function testManyToOneTwoWayRelationship(): void ); // Get document with new relationship key - $store = $this->getDatabase()->getDocument('store', 'store2'); + $store = static::getDatabase()->getDocument('store', 'store2'); $products = $store->getAttribute('newProducts'); $this->assertEquals('product1', $products[0]['$id']); // Get inverse document with new relationship key - $product = $this->getDatabase()->getDocument('product', 'product1'); + $product = static::getDatabase()->getDocument('product', 'product1'); $store = $product->getAttribute('newStore'); $this->assertEquals('store2', $store['$id']); // Reset relationships - $store1 = $this->getDatabase()->getDocument('store', 'store1'); - $this->getDatabase()->updateDocument( + $store1 = static::getDatabase()->getDocument('store', 'store1'); + static::getDatabase()->updateDocument( 'store', $store1->getId(), $store1->setAttribute('newProducts', ['product1']) ); // Create new document with no relationship - $this->getDatabase()->createDocument('store', new Document([ + static::getDatabase()->createDocument('store', new Document([ '$id' => 'store7', '$permissions' => [ Permission::read(Role::any()), @@ -9013,87 +9053,87 @@ public function testManyToOneTwoWayRelationship(): void ])); // Can delete document with no relationship when on delete is set to restrict - $deleted = $this->getDatabase()->deleteDocument('store', 'store7'); + $deleted = static::getDatabase()->deleteDocument('store', 'store7'); $this->assertEquals(true, $deleted); - $store7 = $this->getDatabase()->getDocument('store', 'store7'); + $store7 = static::getDatabase()->getDocument('store', 'store7'); $this->assertEquals(true, $store7->isEmpty()); // Try to delete child while still related to another with on delete: restrict try { - $this->getDatabase()->deleteDocument('store', 'store1'); + static::getDatabase()->deleteDocument('store', 'store1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Delete parent while still related to another with on delete: restrict - $result = $this->getDatabase()->deleteDocument('product', 'product5'); + $result = static::getDatabase()->deleteDocument('product', 'product5'); $this->assertEquals(true, $result); // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'product', id: 'newStore', onDelete: Database::RELATION_MUTATE_SET_NULL ); // Delete child, set parent relationship to null - $this->getDatabase()->deleteDocument('store', 'store1'); + static::getDatabase()->deleteDocument('store', 'store1'); // Check relation was set to null - $this->getDatabase()->getDocument('product', 'product1'); + static::getDatabase()->getDocument('product', 'product1'); $this->assertEquals(null, $product1->getAttribute('newStore')); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'product', id: 'newStore', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete child, will delete parent - $this->getDatabase()->deleteDocument('store', 'store2'); + static::getDatabase()->deleteDocument('store', 'store2'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('store', 'store2'); + $library = static::getDatabase()->getDocument('store', 'store2'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('product', 'product2'); + $library = static::getDatabase()->getDocument('product', 'product2'); $this->assertEquals(true, $library->isEmpty()); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'product', 'newStore' ); // Try to get document again - $products = $this->getDatabase()->getDocument('product', 'product1'); + $products = static::getDatabase()->getDocument('product', 'product1'); $store = $products->getAttribute('newStore'); $this->assertEquals(null, $store); // Try to get inverse document again - $store = $this->getDatabase()->getDocument('store', 'store1'); + $store = static::getDatabase()->getDocument('store', 'store1'); $products = $store->getAttribute('newProducts'); $this->assertEquals(null, $products); } public function testManyToManyOneWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('playlist'); - $this->getDatabase()->createCollection('song'); + static::getDatabase()->createCollection('playlist'); + static::getDatabase()->createCollection('song'); - $this->getDatabase()->createAttribute('playlist', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('song', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('song', 'length', Database::VAR_INTEGER, 0, true); + static::getDatabase()->createAttribute('playlist', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('song', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('song', 'length', Database::VAR_INTEGER, 0, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'playlist', relatedCollection: 'song', type: Database::RELATION_MANY_TO_MANY, @@ -9101,7 +9141,7 @@ public function testManyToManyOneWayRelationship(): void ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('playlist'); + $collection = static::getDatabase()->getCollection('playlist'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { @@ -9117,7 +9157,7 @@ public function testManyToManyOneWayRelationship(): void } // Create document with relationship with nested data - $playlist1 = $this->getDatabase()->createDocument('playlist', new Document([ + $playlist1 = static::getDatabase()->createDocument('playlist', new Document([ '$id' => 'playlist1', '$permissions' => [ Permission::read(Role::any()), @@ -9140,7 +9180,7 @@ public function testManyToManyOneWayRelationship(): void ])); // Create document with relationship with related ID - $this->getDatabase()->createDocument('song', new Document([ + static::getDatabase()->createDocument('song', new Document([ '$id' => 'song2', '$permissions' => [ Permission::read(Role::any()), @@ -9150,7 +9190,7 @@ public function testManyToManyOneWayRelationship(): void 'name' => 'Song 2', 'length' => 140, ])); - $this->getDatabase()->createDocument('playlist', new Document([ + static::getDatabase()->createDocument('playlist', new Document([ '$id' => 'playlist2', '$permissions' => [ Permission::read(Role::any()), @@ -9164,13 +9204,13 @@ public function testManyToManyOneWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('playlist', 'playlist1', $playlist1->setAttribute('songs', ['song1','no-song'])); + static::getDatabase()->updateDocument('playlist', 'playlist1', $playlist1->setAttribute('songs', ['song1','no-song'])); - $playlist1Document = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist1Document = static::getDatabase()->getDocument('playlist', 'playlist1'); // Assert document does not contain non existing relation document. $this->assertEquals(1, \count($playlist1Document->getAttribute('songs'))); - $documents = $this->getDatabase()->find('playlist', [ + $documents = static::getDatabase()->find('playlist', [ Query::select(['name']), Query::limit(1) ]); @@ -9178,29 +9218,29 @@ public function testManyToManyOneWayRelationship(): void $this->assertArrayNotHasKey('songs', $documents[0]); // Get document with relationship - $playlist = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist = static::getDatabase()->getDocument('playlist', 'playlist1'); $songs = $playlist->getAttribute('songs', []); $this->assertEquals('song1', $songs[0]['$id']); $this->assertArrayNotHasKey('playlist', $songs[0]); - $playlist = $this->getDatabase()->getDocument('playlist', 'playlist2'); + $playlist = static::getDatabase()->getDocument('playlist', 'playlist2'); $songs = $playlist->getAttribute('songs', []); $this->assertEquals('song2', $songs[0]['$id']); $this->assertArrayNotHasKey('playlist', $songs[0]); // Get related document - $library = $this->getDatabase()->getDocument('song', 'song1'); + $library = static::getDatabase()->getDocument('song', 'song1'); $this->assertArrayNotHasKey('songs', $library); - $library = $this->getDatabase()->getDocument('song', 'song2'); + $library = static::getDatabase()->getDocument('song', 'song2'); $this->assertArrayNotHasKey('songs', $library); - $playlists = $this->getDatabase()->find('playlist'); + $playlists = static::getDatabase()->find('playlist'); $this->assertEquals(2, \count($playlists)); // Select related document attributes - $playlist = $this->getDatabase()->findOne('playlist', [ + $playlist = static::getDatabase()->findOne('playlist', [ Query::select(['*', 'songs.name']) ]); @@ -9211,7 +9251,7 @@ public function testManyToManyOneWayRelationship(): void $this->assertEquals('Song 1', $playlist->getAttribute('songs')[0]->getAttribute('name')); $this->assertArrayNotHasKey('length', $playlist->getAttribute('songs')[0]); - $playlist = $this->getDatabase()->getDocument('playlist', 'playlist1', [ + $playlist = static::getDatabase()->getDocument('playlist', 'playlist1', [ Query::select(['*', 'songs.name']) ]); @@ -9219,32 +9259,32 @@ public function testManyToManyOneWayRelationship(): void $this->assertArrayNotHasKey('length', $playlist->getAttribute('songs')[0]); // Update root document attribute without altering relationship - $playlist1 = $this->getDatabase()->updateDocument( + $playlist1 = static::getDatabase()->updateDocument( 'playlist', $playlist1->getId(), $playlist1->setAttribute('name', 'Playlist 1 Updated') ); $this->assertEquals('Playlist 1 Updated', $playlist1->getAttribute('name')); - $playlist1 = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist1 = static::getDatabase()->getDocument('playlist', 'playlist1'); $this->assertEquals('Playlist 1 Updated', $playlist1->getAttribute('name')); // Update nested document attribute $songs = $playlist1->getAttribute('songs', []); $songs[0]->setAttribute('name', 'Song 1 Updated'); - $playlist1 = $this->getDatabase()->updateDocument( + $playlist1 = static::getDatabase()->updateDocument( 'playlist', $playlist1->getId(), $playlist1->setAttribute('songs', $songs) ); $this->assertEquals('Song 1 Updated', $playlist1->getAttribute('songs')[0]->getAttribute('name')); - $playlist1 = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist1 = static::getDatabase()->getDocument('playlist', 'playlist1'); $this->assertEquals('Song 1 Updated', $playlist1->getAttribute('songs')[0]->getAttribute('name')); // Create new document with no relationship - $playlist5 = $this->getDatabase()->createDocument('playlist', new Document([ + $playlist5 = static::getDatabase()->createDocument('playlist', new Document([ '$id' => 'playlist5', '$permissions' => [ Permission::read(Role::any()), @@ -9255,7 +9295,7 @@ public function testManyToManyOneWayRelationship(): void ])); // Update to relate to created document - $playlist5 = $this->getDatabase()->updateDocument( + $playlist5 = static::getDatabase()->updateDocument( 'playlist', $playlist5->getId(), $playlist5->setAttribute('songs', [new Document([ @@ -9271,7 +9311,7 @@ public function testManyToManyOneWayRelationship(): void ); // Playlist relating to existing songs that belong to other playlists - $this->getDatabase()->createDocument('playlist', new Document([ + static::getDatabase()->createDocument('playlist', new Document([ '$id' => 'playlist6', '$permissions' => [ Permission::read(Role::any()), @@ -9287,30 +9327,30 @@ public function testManyToManyOneWayRelationship(): void ])); $this->assertEquals('Song 5', $playlist5->getAttribute('songs')[0]->getAttribute('name')); - $playlist5 = $this->getDatabase()->getDocument('playlist', 'playlist5'); + $playlist5 = static::getDatabase()->getDocument('playlist', 'playlist5'); $this->assertEquals('Song 5', $playlist5->getAttribute('songs')[0]->getAttribute('name')); // Update document with new related document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'playlist', $playlist1->getId(), $playlist1->setAttribute('songs', ['song2']) ); // Rename relationship key - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'playlist', 'songs', 'newSongs' ); // Get document with new relationship key - $playlist = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist = static::getDatabase()->getDocument('playlist', 'playlist1'); $songs = $playlist->getAttribute('newSongs'); $this->assertEquals('song2', $songs[0]['$id']); // Create new document with no relationship - $this->getDatabase()->createDocument('playlist', new Document([ + static::getDatabase()->createDocument('playlist', new Document([ '$id' => 'playlist3', '$permissions' => [ Permission::read(Role::any()), @@ -9321,87 +9361,87 @@ public function testManyToManyOneWayRelationship(): void ])); // Can delete document with no relationship when on delete is set to restrict - $deleted = $this->getDatabase()->deleteDocument('playlist', 'playlist3'); + $deleted = static::getDatabase()->deleteDocument('playlist', 'playlist3'); $this->assertEquals(true, $deleted); - $playlist3 = $this->getDatabase()->getDocument('playlist', 'playlist3'); + $playlist3 = static::getDatabase()->getDocument('playlist', 'playlist3'); $this->assertEquals(true, $playlist3->isEmpty()); // Try to delete document while still related to another with on delete: restrict try { - $this->getDatabase()->deleteDocument('playlist', 'playlist1'); + static::getDatabase()->deleteDocument('playlist', 'playlist1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'playlist', id: 'newSongs', onDelete: Database::RELATION_MUTATE_SET_NULL ); - $playlist1 = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist1 = static::getDatabase()->getDocument('playlist', 'playlist1'); // Reset relationships - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'playlist', $playlist1->getId(), $playlist1->setAttribute('newSongs', ['song1']) ); // Delete child, will delete junction - $this->getDatabase()->deleteDocument('song', 'song1'); + static::getDatabase()->deleteDocument('song', 'song1'); // Check relation was set to null - $playlist1 = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist1 = static::getDatabase()->getDocument('playlist', 'playlist1'); $this->assertEquals(0, \count($playlist1->getAttribute('newSongs'))); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'playlist', id: 'newSongs', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete parent, will delete child - $this->getDatabase()->deleteDocument('playlist', 'playlist2'); + static::getDatabase()->deleteDocument('playlist', 'playlist2'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('playlist', 'playlist2'); + $library = static::getDatabase()->getDocument('playlist', 'playlist2'); $this->assertEquals(true, $library->isEmpty()); - $library = $this->getDatabase()->getDocument('song', 'song2'); + $library = static::getDatabase()->getDocument('song', 'song2'); $this->assertEquals(true, $library->isEmpty()); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'playlist', 'newSongs' ); // Try to get document again - $playlist = $this->getDatabase()->getDocument('playlist', 'playlist1'); + $playlist = static::getDatabase()->getDocument('playlist', 'playlist1'); $songs = $playlist->getAttribute('newSongs'); $this->assertEquals(null, $songs); } public function testManyToManyTwoWayRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('students'); - $this->getDatabase()->createCollection('classes'); + static::getDatabase()->createCollection('students'); + static::getDatabase()->createCollection('classes'); - $this->getDatabase()->createAttribute('students', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('classes', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('classes', 'number', Database::VAR_INTEGER, 0, true); + static::getDatabase()->createAttribute('students', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('classes', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('classes', 'number', Database::VAR_INTEGER, 0, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'students', relatedCollection: 'classes', type: Database::RELATION_MANY_TO_MANY, @@ -9409,7 +9449,7 @@ public function testManyToManyTwoWayRelationship(): void ); // Check metadata for collection - $collection = $this->getDatabase()->getCollection('students'); + $collection = static::getDatabase()->getCollection('students'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'students') { @@ -9424,7 +9464,7 @@ public function testManyToManyTwoWayRelationship(): void } // Check metadata for related collection - $collection = $this->getDatabase()->getCollection('classes'); + $collection = static::getDatabase()->getCollection('classes'); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { if ($attribute['key'] === 'classes') { @@ -9439,7 +9479,7 @@ public function testManyToManyTwoWayRelationship(): void } // Create document with relationship with nested data - $student1 = $this->getDatabase()->createDocument('students', new Document([ + $student1 = static::getDatabase()->createDocument('students', new Document([ '$id' => 'student1', '$permissions' => [ Permission::read(Role::any()), @@ -9462,14 +9502,14 @@ public function testManyToManyTwoWayRelationship(): void ])); // Update a document with non existing related document. It should not get added to the list. - $this->getDatabase()->updateDocument('students', 'student1', $student1->setAttribute('classes', ['class1', 'no-class'])); + static::getDatabase()->updateDocument('students', 'student1', $student1->setAttribute('classes', ['class1', 'no-class'])); - $student1Document = $this->getDatabase()->getDocument('students', 'student1'); + $student1Document = static::getDatabase()->getDocument('students', 'student1'); // Assert document does not contain non existing relation document. $this->assertEquals(1, \count($student1Document->getAttribute('classes'))); // Create document with relationship with related ID - $this->getDatabase()->createDocument('classes', new Document([ + static::getDatabase()->createDocument('classes', new Document([ '$id' => 'class2', '$permissions' => [ Permission::read(Role::any()), @@ -9480,7 +9520,7 @@ public function testManyToManyTwoWayRelationship(): void 'name' => 'Class 2', 'number' => 2, ])); - $this->getDatabase()->createDocument('students', new Document([ + static::getDatabase()->createDocument('students', new Document([ '$id' => 'student2', '$permissions' => [ Permission::read(Role::any()), @@ -9494,7 +9534,7 @@ public function testManyToManyTwoWayRelationship(): void ])); // Create from child side - $this->getDatabase()->createDocument('classes', new Document([ + static::getDatabase()->createDocument('classes', new Document([ '$id' => 'class3', '$permissions' => [ Permission::read(Role::any()), @@ -9515,7 +9555,7 @@ public function testManyToManyTwoWayRelationship(): void ] ], ])); - $this->getDatabase()->createDocument('students', new Document([ + static::getDatabase()->createDocument('students', new Document([ '$id' => 'student4', '$permissions' => [ Permission::read(Role::any()), @@ -9524,7 +9564,7 @@ public function testManyToManyTwoWayRelationship(): void ], 'name' => 'Student 4' ])); - $this->getDatabase()->createDocument('classes', new Document([ + static::getDatabase()->createDocument('classes', new Document([ '$id' => 'class4', '$permissions' => [ Permission::read(Role::any()), @@ -9540,49 +9580,49 @@ public function testManyToManyTwoWayRelationship(): void ])); // Get document with relationship - $student = $this->getDatabase()->getDocument('students', 'student1'); + $student = static::getDatabase()->getDocument('students', 'student1'); $classes = $student->getAttribute('classes', []); $this->assertEquals('class1', $classes[0]['$id']); $this->assertArrayNotHasKey('students', $classes[0]); - $student = $this->getDatabase()->getDocument('students', 'student2'); + $student = static::getDatabase()->getDocument('students', 'student2'); $classes = $student->getAttribute('classes', []); $this->assertEquals('class2', $classes[0]['$id']); $this->assertArrayNotHasKey('students', $classes[0]); - $student = $this->getDatabase()->getDocument('students', 'student3'); + $student = static::getDatabase()->getDocument('students', 'student3'); $classes = $student->getAttribute('classes', []); $this->assertEquals('class3', $classes[0]['$id']); $this->assertArrayNotHasKey('students', $classes[0]); - $student = $this->getDatabase()->getDocument('students', 'student4'); + $student = static::getDatabase()->getDocument('students', 'student4'); $classes = $student->getAttribute('classes', []); $this->assertEquals('class4', $classes[0]['$id']); $this->assertArrayNotHasKey('students', $classes[0]); // Get related document - $class = $this->getDatabase()->getDocument('classes', 'class1'); + $class = static::getDatabase()->getDocument('classes', 'class1'); $student = $class->getAttribute('students'); $this->assertEquals('student1', $student[0]['$id']); $this->assertArrayNotHasKey('classes', $student[0]); - $class = $this->getDatabase()->getDocument('classes', 'class2'); + $class = static::getDatabase()->getDocument('classes', 'class2'); $student = $class->getAttribute('students'); $this->assertEquals('student2', $student[0]['$id']); $this->assertArrayNotHasKey('classes', $student[0]); - $class = $this->getDatabase()->getDocument('classes', 'class3'); + $class = static::getDatabase()->getDocument('classes', 'class3'); $student = $class->getAttribute('students'); $this->assertEquals('student3', $student[0]['$id']); $this->assertArrayNotHasKey('classes', $student[0]); - $class = $this->getDatabase()->getDocument('classes', 'class4'); + $class = static::getDatabase()->getDocument('classes', 'class4'); $student = $class->getAttribute('students'); $this->assertEquals('student4', $student[0]['$id']); $this->assertArrayNotHasKey('classes', $student[0]); // Select related document attributes - $student = $this->getDatabase()->findOne('students', [ + $student = static::getDatabase()->findOne('students', [ Query::select(['*', 'classes.name']) ]); @@ -9593,7 +9633,7 @@ public function testManyToManyTwoWayRelationship(): void $this->assertEquals('Class 1', $student->getAttribute('classes')[0]->getAttribute('name')); $this->assertArrayNotHasKey('number', $student->getAttribute('classes')[0]); - $student = $this->getDatabase()->getDocument('students', 'student1', [ + $student = static::getDatabase()->getDocument('students', 'student1', [ Query::select(['*', 'classes.name']) ]); @@ -9601,58 +9641,58 @@ public function testManyToManyTwoWayRelationship(): void $this->assertArrayNotHasKey('number', $student->getAttribute('classes')[0]); // Update root document attribute without altering relationship - $student1 = $this->getDatabase()->updateDocument( + $student1 = static::getDatabase()->updateDocument( 'students', $student1->getId(), $student1->setAttribute('name', 'Student 1 Updated') ); $this->assertEquals('Student 1 Updated', $student1->getAttribute('name')); - $student1 = $this->getDatabase()->getDocument('students', 'student1'); + $student1 = static::getDatabase()->getDocument('students', 'student1'); $this->assertEquals('Student 1 Updated', $student1->getAttribute('name')); // Update inverse root document attribute without altering relationship - $class2 = $this->getDatabase()->getDocument('classes', 'class2'); - $class2 = $this->getDatabase()->updateDocument( + $class2 = static::getDatabase()->getDocument('classes', 'class2'); + $class2 = static::getDatabase()->updateDocument( 'classes', $class2->getId(), $class2->setAttribute('name', 'Class 2 Updated') ); $this->assertEquals('Class 2 Updated', $class2->getAttribute('name')); - $class2 = $this->getDatabase()->getDocument('classes', 'class2'); + $class2 = static::getDatabase()->getDocument('classes', 'class2'); $this->assertEquals('Class 2 Updated', $class2->getAttribute('name')); // Update nested document attribute $classes = $student1->getAttribute('classes', []); $classes[0]->setAttribute('name', 'Class 1 Updated'); - $student1 = $this->getDatabase()->updateDocument( + $student1 = static::getDatabase()->updateDocument( 'students', $student1->getId(), $student1->setAttribute('classes', $classes) ); $this->assertEquals('Class 1 Updated', $student1->getAttribute('classes')[0]->getAttribute('name')); - $student1 = $this->getDatabase()->getDocument('students', 'student1'); + $student1 = static::getDatabase()->getDocument('students', 'student1'); $this->assertEquals('Class 1 Updated', $student1->getAttribute('classes')[0]->getAttribute('name')); // Update inverse nested document attribute $students = $class2->getAttribute('students', []); $students[0]->setAttribute('name', 'Student 2 Updated'); - $class2 = $this->getDatabase()->updateDocument( + $class2 = static::getDatabase()->updateDocument( 'classes', $class2->getId(), $class2->setAttribute('students', $students) ); $this->assertEquals('Student 2 Updated', $class2->getAttribute('students')[0]->getAttribute('name')); - $class2 = $this->getDatabase()->getDocument('classes', 'class2'); + $class2 = static::getDatabase()->getDocument('classes', 'class2'); $this->assertEquals('Student 2 Updated', $class2->getAttribute('students')[0]->getAttribute('name')); // Create new document with no relationship - $student5 = $this->getDatabase()->createDocument('students', new Document([ + $student5 = static::getDatabase()->createDocument('students', new Document([ '$id' => 'student5', '$permissions' => [ Permission::read(Role::any()), @@ -9663,7 +9703,7 @@ public function testManyToManyTwoWayRelationship(): void ])); // Update to relate to created document - $student5 = $this->getDatabase()->updateDocument( + $student5 = static::getDatabase()->updateDocument( 'students', $student5->getId(), $student5->setAttribute('classes', [new Document([ @@ -9679,11 +9719,11 @@ public function testManyToManyTwoWayRelationship(): void ); $this->assertEquals('Class 5', $student5->getAttribute('classes')[0]->getAttribute('name')); - $student5 = $this->getDatabase()->getDocument('students', 'student5'); + $student5 = static::getDatabase()->getDocument('students', 'student5'); $this->assertEquals('Class 5', $student5->getAttribute('classes')[0]->getAttribute('name')); // Create child document with no relationship - $class6 = $this->getDatabase()->createDocument('classes', new Document([ + $class6 = static::getDatabase()->createDocument('classes', new Document([ '$id' => 'class6', '$permissions' => [ Permission::read(Role::any()), @@ -9695,7 +9735,7 @@ public function testManyToManyTwoWayRelationship(): void ])); // Update to relate to created document - $class6 = $this->getDatabase()->updateDocument( + $class6 = static::getDatabase()->updateDocument( 'classes', $class6->getId(), $class6->setAttribute('students', [new Document([ @@ -9710,27 +9750,27 @@ public function testManyToManyTwoWayRelationship(): void ); $this->assertEquals('Student 6', $class6->getAttribute('students')[0]->getAttribute('name')); - $class6 = $this->getDatabase()->getDocument('classes', 'class6'); + $class6 = static::getDatabase()->getDocument('classes', 'class6'); $this->assertEquals('Student 6', $class6->getAttribute('students')[0]->getAttribute('name')); // Update document with new related document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'students', $student1->getId(), $student1->setAttribute('classes', ['class2']) ); - $class1 = $this->getDatabase()->getDocument('classes', 'class1'); + $class1 = static::getDatabase()->getDocument('classes', 'class1'); // Update inverse document - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'classes', $class1->getId(), $class1->setAttribute('students', ['student1']) ); // Rename relationship keys on both sides - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( 'students', 'classes', 'newClasses', @@ -9738,17 +9778,17 @@ public function testManyToManyTwoWayRelationship(): void ); // Get document with new relationship key - $students = $this->getDatabase()->getDocument('students', 'student1'); + $students = static::getDatabase()->getDocument('students', 'student1'); $classes = $students->getAttribute('newClasses'); $this->assertEquals('class2', $classes[0]['$id']); // Get inverse document with new relationship key - $class = $this->getDatabase()->getDocument('classes', 'class1'); + $class = static::getDatabase()->getDocument('classes', 'class1'); $students = $class->getAttribute('newStudents'); $this->assertEquals('student1', $students[0]['$id']); // Create new document with no relationship - $this->getDatabase()->createDocument('students', new Document([ + static::getDatabase()->createDocument('students', new Document([ '$id' => 'student7', '$permissions' => [ Permission::read(Role::any()), @@ -9759,107 +9799,107 @@ public function testManyToManyTwoWayRelationship(): void ])); // Can delete document with no relationship when on delete is set to restrict - $deleted = $this->getDatabase()->deleteDocument('students', 'student7'); + $deleted = static::getDatabase()->deleteDocument('students', 'student7'); $this->assertEquals(true, $deleted); - $student6 = $this->getDatabase()->getDocument('students', 'student7'); + $student6 = static::getDatabase()->getDocument('students', 'student7'); $this->assertEquals(true, $student6->isEmpty()); // Try to delete document while still related to another with on delete: restrict try { - $this->getDatabase()->deleteDocument('students', 'student1'); + static::getDatabase()->deleteDocument('students', 'student1'); $this->fail('Failed to throw exception'); } catch (Exception $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Change on delete to set null - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'students', id: 'newClasses', onDelete: Database::RELATION_MUTATE_SET_NULL ); - $student1 = $this->getDatabase()->getDocument('students', 'student1'); + $student1 = static::getDatabase()->getDocument('students', 'student1'); // Reset relationships - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'students', $student1->getId(), $student1->setAttribute('newClasses', ['class1']) ); // Delete child, will delete junction - $this->getDatabase()->deleteDocument('classes', 'class1'); + static::getDatabase()->deleteDocument('classes', 'class1'); // Check relation was set to null - $student1 = $this->getDatabase()->getDocument('students', 'student1'); + $student1 = static::getDatabase()->getDocument('students', 'student1'); $this->assertEquals(0, \count($student1->getAttribute('newClasses'))); // Change on delete to cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'students', id: 'newClasses', onDelete: Database::RELATION_MUTATE_CASCADE ); // Delete parent, will delete child - $this->getDatabase()->deleteDocument('students', 'student2'); + static::getDatabase()->deleteDocument('students', 'student2'); // Check parent and child were deleted - $library = $this->getDatabase()->getDocument('students', 'student2'); + $library = static::getDatabase()->getDocument('students', 'student2'); $this->assertEquals(true, $library->isEmpty()); // Delete child, should not delete parent - $this->getDatabase()->deleteDocument('classes', 'class6'); + static::getDatabase()->deleteDocument('classes', 'class6'); // Check only child was deleted - $student6 = $this->getDatabase()->getDocument('students', 'student6'); + $student6 = static::getDatabase()->getDocument('students', 'student6'); $this->assertEquals(false, $student6->isEmpty()); $this->assertEmpty($student6->getAttribute('newClasses')); - $library = $this->getDatabase()->getDocument('classes', 'class2'); + $library = static::getDatabase()->getDocument('classes', 'class2'); $this->assertEquals(true, $library->isEmpty()); // Delete relationship - $this->getDatabase()->deleteRelationship( + static::getDatabase()->deleteRelationship( 'students', 'newClasses' ); // Try to get documents again - $student = $this->getDatabase()->getDocument('students', 'student1'); + $student = static::getDatabase()->getDocument('students', 'student1'); $classes = $student->getAttribute('newClasses'); $this->assertEquals(null, $classes); // Try to get inverse documents again - $classes = $this->getDatabase()->getDocument('classes', 'class1'); + $classes = static::getDatabase()->getDocument('classes', 'class1'); $students = $classes->getAttribute('newStudents'); $this->assertEquals(null, $students); } public function testSelectRelationshipAttributes(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('make'); - $this->getDatabase()->createCollection('model'); + static::getDatabase()->createCollection('make'); + static::getDatabase()->createCollection('model'); - $this->getDatabase()->createAttribute('make', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('model', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('model', 'year', Database::VAR_INTEGER, 0, true); + static::getDatabase()->createAttribute('make', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('model', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('model', 'year', Database::VAR_INTEGER, 0, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'make', relatedCollection: 'model', type: Database::RELATION_ONE_TO_MANY, id: 'models' ); - $this->getDatabase()->createDocument('make', new Document([ + static::getDatabase()->createDocument('make', new Document([ '$id' => 'ford', '$permissions' => [ Permission::read(Role::any()), @@ -9886,7 +9926,7 @@ public function testSelectRelationshipAttributes(): void ])); // Select some parent attributes, some child attributes - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', 'models.name']), ]); @@ -9908,7 +9948,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$updatedAt', $make); // Select internal attributes - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', '$id']), ]); @@ -9923,7 +9963,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$updatedAt', $make); $this->assertArrayNotHasKey('$permissions', $make); - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', '$internalId']), ]); @@ -9938,7 +9978,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$updatedAt', $make); $this->assertArrayNotHasKey('$permissions', $make); - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', '$collection']), ]); @@ -9953,7 +9993,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$updatedAt', $make); $this->assertArrayNotHasKey('$permissions', $make); - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', '$createdAt']), ]); @@ -9968,7 +10008,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayNotHasKey('$updatedAt', $make); $this->assertArrayNotHasKey('$permissions', $make); - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', '$updatedAt']), ]); @@ -9983,7 +10023,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayHasKey('$updatedAt', $make); $this->assertArrayNotHasKey('$permissions', $make); - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name', '$permissions']), ]); @@ -9999,7 +10039,7 @@ public function testSelectRelationshipAttributes(): void $this->assertArrayHasKey('$permissions', $make); // Select all parent attributes, some child attributes - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['*', 'models.year']), ]); @@ -10015,7 +10055,7 @@ public function testSelectRelationshipAttributes(): void $this->assertEquals(2011, $make['models'][1]['year']); // Select all parent attributes, all child attributes - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['*', 'models.*']), ]); @@ -10032,7 +10072,7 @@ public function testSelectRelationshipAttributes(): void // Select all parent attributes, all child attributes // Must select parent if selecting children - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['models.*']), ]); @@ -10048,7 +10088,7 @@ public function testSelectRelationshipAttributes(): void $this->assertEquals(2011, $make['models'][1]['year']); // Select all parent attributes, no child attributes - $make = $this->getDatabase()->findOne('make', [ + $make = static::getDatabase()->findOne('make', [ Query::select(['name']), ]); @@ -10062,20 +10102,20 @@ public function testSelectRelationshipAttributes(): void public function testNestedOneToOne_OneToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('pattern'); - $this->getDatabase()->createCollection('shirt'); - $this->getDatabase()->createCollection('team'); + static::getDatabase()->createCollection('pattern'); + static::getDatabase()->createCollection('shirt'); + static::getDatabase()->createCollection('team'); - $this->getDatabase()->createAttribute('pattern', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('shirt', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('team', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('pattern', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('shirt', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('team', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'pattern', relatedCollection: 'shirt', type: Database::RELATION_ONE_TO_ONE, @@ -10083,7 +10123,7 @@ public function testNestedOneToOne_OneToOneRelationship(): void id: 'shirt', twoWayKey: 'pattern' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'shirt', relatedCollection: 'team', type: Database::RELATION_ONE_TO_ONE, @@ -10092,7 +10132,7 @@ public function testNestedOneToOne_OneToOneRelationship(): void twoWayKey: 'shirt' ); - $this->getDatabase()->createDocument('pattern', new Document([ + static::getDatabase()->createDocument('pattern', new Document([ '$id' => 'stripes', '$permissions' => [ Permission::read(Role::any()), @@ -10114,13 +10154,13 @@ public function testNestedOneToOne_OneToOneRelationship(): void ], ])); - $pattern = $this->getDatabase()->getDocument('pattern', 'stripes'); + $pattern = static::getDatabase()->getDocument('pattern', 'stripes'); $this->assertEquals('red', $pattern['shirt']['$id']); $this->assertArrayNotHasKey('pattern', $pattern['shirt']); $this->assertEquals('reds', $pattern['shirt']['team']['$id']); $this->assertArrayNotHasKey('shirt', $pattern['shirt']['team']); - $this->getDatabase()->createDocument('team', new Document([ + static::getDatabase()->createDocument('team', new Document([ '$id' => 'blues', '$permissions' => [ Permission::read(Role::any()), @@ -10142,7 +10182,7 @@ public function testNestedOneToOne_OneToOneRelationship(): void ], ])); - $team = $this->getDatabase()->getDocument('team', 'blues'); + $team = static::getDatabase()->getDocument('team', 'blues'); $this->assertEquals('blue', $team['shirt']['$id']); $this->assertArrayNotHasKey('team', $team['shirt']); $this->assertEquals('plain', $team['shirt']['pattern']['$id']); @@ -10151,20 +10191,20 @@ public function testNestedOneToOne_OneToOneRelationship(): void public function testNestedOneToOne_OneToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('teachers'); - $this->getDatabase()->createCollection('classrooms'); - $this->getDatabase()->createCollection('children'); + static::getDatabase()->createCollection('teachers'); + static::getDatabase()->createCollection('classrooms'); + static::getDatabase()->createCollection('children'); - $this->getDatabase()->createAttribute('children', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('teachers', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('classrooms', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('children', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('teachers', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('classrooms', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'teachers', relatedCollection: 'classrooms', type: Database::RELATION_ONE_TO_ONE, @@ -10172,7 +10212,7 @@ public function testNestedOneToOne_OneToManyRelationship(): void id: 'classroom', twoWayKey: 'teacher' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'classrooms', relatedCollection: 'children', type: Database::RELATION_ONE_TO_MANY, @@ -10180,7 +10220,7 @@ public function testNestedOneToOne_OneToManyRelationship(): void twoWayKey: 'classroom' ); - $this->getDatabase()->createDocument('teachers', new Document([ + static::getDatabase()->createDocument('teachers', new Document([ '$id' => 'teacher1', '$permissions' => [ Permission::read(Role::any()), @@ -10211,14 +10251,14 @@ public function testNestedOneToOne_OneToManyRelationship(): void ], ])); - $teacher1 = $this->getDatabase()->getDocument('teachers', 'teacher1'); + $teacher1 = static::getDatabase()->getDocument('teachers', 'teacher1'); $this->assertEquals('classroom1', $teacher1['classroom']['$id']); $this->assertArrayNotHasKey('teacher', $teacher1['classroom']); $this->assertEquals(2, \count($teacher1['classroom']['children'])); $this->assertEquals('Child 1', $teacher1['classroom']['children'][0]['name']); $this->assertEquals('Child 2', $teacher1['classroom']['children'][1]['name']); - $this->getDatabase()->createDocument('children', new Document([ + static::getDatabase()->createDocument('children', new Document([ '$id' => 'child3', '$permissions' => [ Permission::read(Role::any()), @@ -10240,7 +10280,7 @@ public function testNestedOneToOne_OneToManyRelationship(): void ], ])); - $child3 = $this->getDatabase()->getDocument('children', 'child3'); + $child3 = static::getDatabase()->getDocument('children', 'child3'); $this->assertEquals('classroom2', $child3['classroom']['$id']); $this->assertArrayNotHasKey('children', $child3['classroom']); $this->assertEquals('teacher2', $child3['classroom']['teacher']['$id']); @@ -10249,20 +10289,20 @@ public function testNestedOneToOne_OneToManyRelationship(): void public function testNestedOneToOne_ManyToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('users'); - $this->getDatabase()->createCollection('profiles'); - $this->getDatabase()->createCollection('avatars'); + static::getDatabase()->createCollection('users'); + static::getDatabase()->createCollection('profiles'); + static::getDatabase()->createCollection('avatars'); - $this->getDatabase()->createAttribute('users', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('profiles', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('avatars', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('users', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('profiles', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('avatars', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'users', relatedCollection: 'profiles', type: Database::RELATION_ONE_TO_ONE, @@ -10270,7 +10310,7 @@ public function testNestedOneToOne_ManyToOneRelationship(): void id: 'profile', twoWayKey: 'user' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'profiles', relatedCollection: 'avatars', type: Database::RELATION_MANY_TO_ONE, @@ -10278,7 +10318,7 @@ public function testNestedOneToOne_ManyToOneRelationship(): void id: 'avatar', ); - $this->getDatabase()->createDocument('users', new Document([ + static::getDatabase()->createDocument('users', new Document([ '$id' => 'user1', '$permissions' => [ Permission::read(Role::any()), @@ -10300,13 +10340,13 @@ public function testNestedOneToOne_ManyToOneRelationship(): void ], ])); - $user1 = $this->getDatabase()->getDocument('users', 'user1'); + $user1 = static::getDatabase()->getDocument('users', 'user1'); $this->assertEquals('profile1', $user1['profile']['$id']); $this->assertArrayNotHasKey('user', $user1['profile']); $this->assertEquals('avatar1', $user1['profile']['avatar']['$id']); $this->assertArrayNotHasKey('profile', $user1['profile']['avatar']); - $this->getDatabase()->createDocument('avatars', new Document([ + static::getDatabase()->createDocument('avatars', new Document([ '$id' => 'avatar2', '$permissions' => [ Permission::read(Role::any()), @@ -10330,7 +10370,7 @@ public function testNestedOneToOne_ManyToOneRelationship(): void ], ])); - $avatar2 = $this->getDatabase()->getDocument('avatars', 'avatar2'); + $avatar2 = static::getDatabase()->getDocument('avatars', 'avatar2'); $this->assertEquals('profile2', $avatar2['profiles'][0]['$id']); $this->assertArrayNotHasKey('avatars', $avatar2['profiles'][0]); $this->assertEquals('user2', $avatar2['profiles'][0]['user']['$id']); @@ -10339,20 +10379,20 @@ public function testNestedOneToOne_ManyToOneRelationship(): void public function testNestedOneToOne_ManyToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('addresses'); - $this->getDatabase()->createCollection('houses'); - $this->getDatabase()->createCollection('buildings'); + static::getDatabase()->createCollection('addresses'); + static::getDatabase()->createCollection('houses'); + static::getDatabase()->createCollection('buildings'); - $this->getDatabase()->createAttribute('addresses', 'street', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('houses', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('buildings', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('addresses', 'street', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('houses', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('buildings', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'addresses', relatedCollection: 'houses', type: Database::RELATION_ONE_TO_ONE, @@ -10360,14 +10400,14 @@ public function testNestedOneToOne_ManyToManyRelationship(): void id: 'house', twoWayKey: 'address' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'houses', relatedCollection: 'buildings', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $this->getDatabase()->createDocument('addresses', new Document([ + static::getDatabase()->createDocument('addresses', new Document([ '$id' => 'address1', '$permissions' => [ Permission::read(Role::any()), @@ -10398,7 +10438,7 @@ public function testNestedOneToOne_ManyToManyRelationship(): void ], ])); - $address1 = $this->getDatabase()->getDocument('addresses', 'address1'); + $address1 = static::getDatabase()->getDocument('addresses', 'address1'); $this->assertEquals('house1', $address1['house']['$id']); $this->assertArrayNotHasKey('address', $address1['house']); $this->assertEquals('building1', $address1['house']['buildings'][0]['$id']); @@ -10406,7 +10446,7 @@ public function testNestedOneToOne_ManyToManyRelationship(): void $this->assertArrayNotHasKey('houses', $address1['house']['buildings'][0]); $this->assertArrayNotHasKey('houses', $address1['house']['buildings'][1]); - $this->getDatabase()->createDocument('buildings', new Document([ + static::getDatabase()->createDocument('buildings', new Document([ '$id' => 'building3', '$permissions' => [ Permission::read(Role::any()), @@ -10433,27 +10473,27 @@ public function testNestedOneToOne_ManyToManyRelationship(): void public function testNestedOneToMany_OneToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('countries'); - $this->getDatabase()->createCollection('cities'); - $this->getDatabase()->createCollection('mayors'); + static::getDatabase()->createCollection('countries'); + static::getDatabase()->createCollection('cities'); + static::getDatabase()->createCollection('mayors'); - $this->getDatabase()->createAttribute('cities', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('countries', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('mayors', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('cities', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('countries', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('mayors', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'countries', relatedCollection: 'cities', type: Database::RELATION_ONE_TO_MANY, twoWay: true, twoWayKey: 'country' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'cities', relatedCollection: 'mayors', type: Database::RELATION_ONE_TO_ONE, @@ -10462,7 +10502,7 @@ public function testNestedOneToMany_OneToOneRelationship(): void twoWayKey: 'city' ); - $this->getDatabase()->createDocument('countries', new Document([ + static::getDatabase()->createDocument('countries', new Document([ '$id' => 'country1', '$permissions' => [ Permission::read(Role::any()), @@ -10503,26 +10543,26 @@ public function testNestedOneToMany_OneToOneRelationship(): void ], ])); - $documents = $this->getDatabase()->find('countries', [ + $documents = static::getDatabase()->find('countries', [ Query::limit(1) ]); $this->assertEquals('Mayor 1', $documents[0]['cities'][0]['mayor']['name']); - $documents = $this->getDatabase()->find('countries', [ + $documents = static::getDatabase()->find('countries', [ Query::select(['name']), Query::limit(1) ]); $this->assertArrayHasKey('name', $documents[0]); $this->assertArrayNotHasKey('cities', $documents[0]); - $documents = $this->getDatabase()->find('countries', [ + $documents = static::getDatabase()->find('countries', [ Query::select(['*']), Query::limit(1) ]); $this->assertArrayHasKey('name', $documents[0]); $this->assertArrayNotHasKey('cities', $documents[0]); - $documents = $this->getDatabase()->find('countries', [ + $documents = static::getDatabase()->find('countries', [ Query::select(['*', 'cities.*', 'cities.mayor.*']), Query::limit(1) ]); @@ -10530,12 +10570,12 @@ public function testNestedOneToMany_OneToOneRelationship(): void $this->assertEquals('Mayor 1', $documents[0]['cities'][0]['mayor']['name']); // Insert docs to cache: - $country1 = $this->getDatabase()->getDocument('countries', 'country1'); - $mayor1 = $this->getDatabase()->getDocument('mayors', 'mayor1'); + $country1 = static::getDatabase()->getDocument('countries', 'country1'); + $mayor1 = static::getDatabase()->getDocument('mayors', 'mayor1'); $this->assertEquals('City 1', $mayor1['city']['name']); $this->assertEquals('City 1', $country1['cities'][0]['name']); - $this->getDatabase()->updateDocument('cities', 'city1', new Document([ + static::getDatabase()->updateDocument('cities', 'city1', new Document([ '$id' => 'city1', '$collection' => 'cities', 'name' => 'City 1 updated', @@ -10546,8 +10586,8 @@ public function testNestedOneToMany_OneToOneRelationship(): void ], ])); - $mayor1 = $this->getDatabase()->getDocument('mayors', 'mayor1'); - $country1 = $this->getDatabase()->getDocument('countries', 'country1'); + $mayor1 = static::getDatabase()->getDocument('mayors', 'mayor1'); + $country1 = static::getDatabase()->getDocument('countries', 'country1'); $this->assertEquals('City 1 updated', $mayor1['city']['name']); $this->assertEquals('City 1 updated', $country1['cities'][0]['name']); @@ -10558,7 +10598,7 @@ public function testNestedOneToMany_OneToOneRelationship(): void $this->assertArrayNotHasKey('city', $country1['cities'][0]['mayor']); $this->assertArrayNotHasKey('city', $country1['cities'][1]['mayor']); - $this->getDatabase()->createDocument('mayors', new Document([ + static::getDatabase()->createDocument('mayors', new Document([ '$id' => 'mayor3', '$permissions' => [ Permission::read(Role::any()), @@ -10580,7 +10620,7 @@ public function testNestedOneToMany_OneToOneRelationship(): void ], ])); - $country2 = $this->getDatabase()->getDocument('countries', 'country2'); + $country2 = static::getDatabase()->getDocument('countries', 'country2'); $this->assertEquals('city3', $country2['cities'][0]['$id']); $this->assertEquals('mayor3', $country2['cities'][0]['mayor']['$id']); $this->assertArrayNotHasKey('country', $country2['cities'][0]); @@ -10589,27 +10629,27 @@ public function testNestedOneToMany_OneToOneRelationship(): void public function testNestedOneToMany_OneToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('dormitories'); - $this->getDatabase()->createCollection('occupants'); - $this->getDatabase()->createCollection('pets'); + static::getDatabase()->createCollection('dormitories'); + static::getDatabase()->createCollection('occupants'); + static::getDatabase()->createCollection('pets'); - $this->getDatabase()->createAttribute('dormitories', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('occupants', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('pets', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('dormitories', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('occupants', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('pets', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'dormitories', relatedCollection: 'occupants', type: Database::RELATION_ONE_TO_MANY, twoWay: true, twoWayKey: 'dormitory' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'occupants', relatedCollection: 'pets', type: Database::RELATION_ONE_TO_MANY, @@ -10617,7 +10657,7 @@ public function testNestedOneToMany_OneToManyRelationship(): void twoWayKey: 'occupant' ); - $this->getDatabase()->createDocument('dormitories', new Document([ + static::getDatabase()->createDocument('dormitories', new Document([ '$id' => 'dormitory1', '$permissions' => [ Permission::read(Role::any()), @@ -10673,7 +10713,7 @@ public function testNestedOneToMany_OneToManyRelationship(): void ], ])); - $dormitory1 = $this->getDatabase()->getDocument('dormitories', 'dormitory1'); + $dormitory1 = static::getDatabase()->getDocument('dormitories', 'dormitory1'); $this->assertEquals('occupant1', $dormitory1['occupants'][0]['$id']); $this->assertEquals('occupant2', $dormitory1['occupants'][1]['$id']); $this->assertEquals('pet1', $dormitory1['occupants'][0]['pets'][0]['$id']); @@ -10687,7 +10727,7 @@ public function testNestedOneToMany_OneToManyRelationship(): void $this->assertArrayNotHasKey('occupant', $dormitory1['occupants'][1]['pets'][0]); $this->assertArrayNotHasKey('occupant', $dormitory1['occupants'][1]['pets'][1]); - $this->getDatabase()->createDocument('pets', new Document([ + static::getDatabase()->createDocument('pets', new Document([ '$id' => 'pet5', '$permissions' => [ Permission::read(Role::any()), @@ -10709,7 +10749,7 @@ public function testNestedOneToMany_OneToManyRelationship(): void ], ])); - $pet5 = $this->getDatabase()->getDocument('pets', 'pet5'); + $pet5 = static::getDatabase()->getDocument('pets', 'pet5'); $this->assertEquals('occupant3', $pet5['occupant']['$id']); $this->assertEquals('dormitory2', $pet5['occupant']['dormitory']['$id']); $this->assertArrayNotHasKey('pets', $pet5['occupant']); @@ -10718,26 +10758,26 @@ public function testNestedOneToMany_OneToManyRelationship(): void public function testNestedOneToMany_ManyToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('home'); - $this->getDatabase()->createCollection('renters'); - $this->getDatabase()->createCollection('floors'); + static::getDatabase()->createCollection('home'); + static::getDatabase()->createCollection('renters'); + static::getDatabase()->createCollection('floors'); - $this->getDatabase()->createAttribute('home', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('renters', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('floors', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('home', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('renters', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('floors', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'home', relatedCollection: 'renters', type: Database::RELATION_ONE_TO_MANY, twoWay: true ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'renters', relatedCollection: 'floors', type: Database::RELATION_MANY_TO_ONE, @@ -10745,7 +10785,7 @@ public function testNestedOneToMany_ManyToOneRelationship(): void id: 'floor' ); - $this->getDatabase()->createDocument('home', new Document([ + static::getDatabase()->createDocument('home', new Document([ '$id' => 'home1', '$permissions' => [ Permission::read(Role::any()), @@ -10769,13 +10809,13 @@ public function testNestedOneToMany_ManyToOneRelationship(): void ], ])); - $home1 = $this->getDatabase()->getDocument('home', 'home1'); + $home1 = static::getDatabase()->getDocument('home', 'home1'); $this->assertEquals('renter1', $home1['renters'][0]['$id']); $this->assertEquals('floor1', $home1['renters'][0]['floor']['$id']); $this->assertArrayNotHasKey('home', $home1['renters'][0]); $this->assertArrayNotHasKey('renters', $home1['renters'][0]['floor']); - $this->getDatabase()->createDocument('floors', new Document([ + static::getDatabase()->createDocument('floors', new Document([ '$id' => 'floor2', '$permissions' => [ Permission::read(Role::any()), @@ -10799,7 +10839,7 @@ public function testNestedOneToMany_ManyToOneRelationship(): void ], ])); - $floor2 = $this->getDatabase()->getDocument('floors', 'floor2'); + $floor2 = static::getDatabase()->getDocument('floors', 'floor2'); $this->assertEquals('renter2', $floor2['renters'][0]['$id']); $this->assertArrayNotHasKey('floor', $floor2['renters'][0]); $this->assertEquals('home2', $floor2['renters'][0]['home']['$id']); @@ -10808,34 +10848,34 @@ public function testNestedOneToMany_ManyToOneRelationship(): void public function testNestedOneToMany_ManyToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('owners'); - $this->getDatabase()->createCollection('cats'); - $this->getDatabase()->createCollection('toys'); + static::getDatabase()->createCollection('owners'); + static::getDatabase()->createCollection('cats'); + static::getDatabase()->createCollection('toys'); - $this->getDatabase()->createAttribute('owners', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('cats', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('toys', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('owners', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('cats', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('toys', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'owners', relatedCollection: 'cats', type: Database::RELATION_ONE_TO_MANY, twoWay: true, twoWayKey: 'owner' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'cats', relatedCollection: 'toys', type: Database::RELATION_MANY_TO_MANY, twoWay: true ); - $this->getDatabase()->createDocument('owners', new Document([ + static::getDatabase()->createDocument('owners', new Document([ '$id' => 'owner1', '$permissions' => [ Permission::read(Role::any()), @@ -10861,13 +10901,13 @@ public function testNestedOneToMany_ManyToManyRelationship(): void ], ])); - $owner1 = $this->getDatabase()->getDocument('owners', 'owner1'); + $owner1 = static::getDatabase()->getDocument('owners', 'owner1'); $this->assertEquals('cat1', $owner1['cats'][0]['$id']); $this->assertArrayNotHasKey('owner', $owner1['cats'][0]); $this->assertEquals('toy1', $owner1['cats'][0]['toys'][0]['$id']); $this->assertArrayNotHasKey('cats', $owner1['cats'][0]['toys'][0]); - $this->getDatabase()->createDocument('toys', new Document([ + static::getDatabase()->createDocument('toys', new Document([ '$id' => 'toy2', '$permissions' => [ Permission::read(Role::any()), @@ -10891,7 +10931,7 @@ public function testNestedOneToMany_ManyToManyRelationship(): void ], ])); - $toy2 = $this->getDatabase()->getDocument('toys', 'toy2'); + $toy2 = static::getDatabase()->getDocument('toys', 'toy2'); $this->assertEquals('cat2', $toy2['cats'][0]['$id']); $this->assertArrayNotHasKey('toys', $toy2['cats'][0]); $this->assertEquals('owner2', $toy2['cats'][0]['owner']['$id']); @@ -10900,27 +10940,27 @@ public function testNestedOneToMany_ManyToManyRelationship(): void public function testNestedManyToOne_OneToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('towns'); - $this->getDatabase()->createCollection('homelands'); - $this->getDatabase()->createCollection('capitals'); + static::getDatabase()->createCollection('towns'); + static::getDatabase()->createCollection('homelands'); + static::getDatabase()->createCollection('capitals'); - $this->getDatabase()->createAttribute('towns', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('homelands', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('capitals', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('towns', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('homelands', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('capitals', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'towns', relatedCollection: 'homelands', type: Database::RELATION_MANY_TO_ONE, twoWay: true, id: 'homeland' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'homelands', relatedCollection: 'capitals', type: Database::RELATION_ONE_TO_ONE, @@ -10929,7 +10969,7 @@ public function testNestedManyToOne_OneToOneRelationship(): void twoWayKey: 'homeland' ); - $this->getDatabase()->createDocument('towns', new Document([ + static::getDatabase()->createDocument('towns', new Document([ '$id' => 'town1', '$permissions' => [ Permission::read(Role::any()), @@ -10951,13 +10991,13 @@ public function testNestedManyToOne_OneToOneRelationship(): void ], ])); - $town1 = $this->getDatabase()->getDocument('towns', 'town1'); + $town1 = static::getDatabase()->getDocument('towns', 'town1'); $this->assertEquals('homeland1', $town1['homeland']['$id']); $this->assertArrayNotHasKey('towns', $town1['homeland']); $this->assertEquals('capital1', $town1['homeland']['capital']['$id']); $this->assertArrayNotHasKey('homeland', $town1['homeland']['capital']); - $this->getDatabase()->createDocument('capitals', new Document([ + static::getDatabase()->createDocument('capitals', new Document([ '$id' => 'capital2', '$permissions' => [ Permission::read(Role::any()), @@ -10988,7 +11028,7 @@ public function testNestedManyToOne_OneToOneRelationship(): void ], ])); - $capital2 = $this->getDatabase()->getDocument('capitals', 'capital2'); + $capital2 = static::getDatabase()->getDocument('capitals', 'capital2'); $this->assertEquals('homeland2', $capital2['homeland']['$id']); $this->assertArrayNotHasKey('capital', $capital2['homeland']); $this->assertEquals(2, \count($capital2['homeland']['towns'])); @@ -10998,27 +11038,27 @@ public function testNestedManyToOne_OneToOneRelationship(): void public function testNestedManyToOne_OneToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('players'); - $this->getDatabase()->createCollection('teams'); - $this->getDatabase()->createCollection('supporters'); + static::getDatabase()->createCollection('players'); + static::getDatabase()->createCollection('teams'); + static::getDatabase()->createCollection('supporters'); - $this->getDatabase()->createAttribute('players', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('teams', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('supporters', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('players', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('teams', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('supporters', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'players', relatedCollection: 'teams', type: Database::RELATION_MANY_TO_ONE, twoWay: true, id: 'team' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'teams', relatedCollection: 'supporters', type: Database::RELATION_ONE_TO_MANY, @@ -11027,7 +11067,7 @@ public function testNestedManyToOne_OneToManyRelationship(): void twoWayKey: 'team' ); - $this->getDatabase()->createDocument('players', new Document([ + static::getDatabase()->createDocument('players', new Document([ '$id' => 'player1', '$permissions' => [ Permission::read(Role::any()), @@ -11058,14 +11098,14 @@ public function testNestedManyToOne_OneToManyRelationship(): void ], ])); - $player1 = $this->getDatabase()->getDocument('players', 'player1'); + $player1 = static::getDatabase()->getDocument('players', 'player1'); $this->assertEquals('team1', $player1['team']['$id']); $this->assertArrayNotHasKey('players', $player1['team']); $this->assertEquals(2, \count($player1['team']['supporters'])); $this->assertEquals('supporter1', $player1['team']['supporters'][0]['$id']); $this->assertEquals('supporter2', $player1['team']['supporters'][1]['$id']); - $this->getDatabase()->createDocument('supporters', new Document([ + static::getDatabase()->createDocument('supporters', new Document([ '$id' => 'supporter3', '$permissions' => [ Permission::read(Role::any()), @@ -11096,7 +11136,7 @@ public function testNestedManyToOne_OneToManyRelationship(): void ], ])); - $supporter3 = $this->getDatabase()->getDocument('supporters', 'supporter3'); + $supporter3 = static::getDatabase()->getDocument('supporters', 'supporter3'); $this->assertEquals('team2', $supporter3['team']['$id']); $this->assertArrayNotHasKey('supporters', $supporter3['team']); $this->assertEquals(2, \count($supporter3['team']['players'])); @@ -11106,27 +11146,27 @@ public function testNestedManyToOne_OneToManyRelationship(): void public function testNestedManyToOne_ManyToOne(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('cows'); - $this->getDatabase()->createCollection('farms'); - $this->getDatabase()->createCollection('farmer'); + static::getDatabase()->createCollection('cows'); + static::getDatabase()->createCollection('farms'); + static::getDatabase()->createCollection('farmer'); - $this->getDatabase()->createAttribute('cows', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('farms', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('farmer', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('cows', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('farms', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('farmer', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'cows', relatedCollection: 'farms', type: Database::RELATION_MANY_TO_ONE, twoWay: true, id: 'farm' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'farms', relatedCollection: 'farmer', type: Database::RELATION_MANY_TO_ONE, @@ -11134,7 +11174,7 @@ public function testNestedManyToOne_ManyToOne(): void id: 'farmer' ); - $this->getDatabase()->createDocument('cows', new Document([ + static::getDatabase()->createDocument('cows', new Document([ '$id' => 'cow1', '$permissions' => [ Permission::read(Role::any()), @@ -11156,13 +11196,13 @@ public function testNestedManyToOne_ManyToOne(): void ], ])); - $cow1 = $this->getDatabase()->getDocument('cows', 'cow1'); + $cow1 = static::getDatabase()->getDocument('cows', 'cow1'); $this->assertEquals('farm1', $cow1['farm']['$id']); $this->assertArrayNotHasKey('cows', $cow1['farm']); $this->assertEquals('farmer1', $cow1['farm']['farmer']['$id']); $this->assertArrayNotHasKey('farms', $cow1['farm']['farmer']); - $this->getDatabase()->createDocument('farmer', new Document([ + static::getDatabase()->createDocument('farmer', new Document([ '$id' => 'farmer2', '$permissions' => [ Permission::read(Role::any()), @@ -11195,7 +11235,7 @@ public function testNestedManyToOne_ManyToOne(): void ], ])); - $farmer2 = $this->getDatabase()->getDocument('farmer', 'farmer2'); + $farmer2 = static::getDatabase()->getDocument('farmer', 'farmer2'); $this->assertEquals('farm2', $farmer2['farms'][0]['$id']); $this->assertArrayNotHasKey('farmer', $farmer2['farms'][0]); $this->assertEquals(2, \count($farmer2['farms'][0]['cows'])); @@ -11205,34 +11245,34 @@ public function testNestedManyToOne_ManyToOne(): void public function testNestedManyToOne_ManyToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('books'); - $this->getDatabase()->createCollection('entrants'); - $this->getDatabase()->createCollection('rooms'); + static::getDatabase()->createCollection('books'); + static::getDatabase()->createCollection('entrants'); + static::getDatabase()->createCollection('rooms'); - $this->getDatabase()->createAttribute('books', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('entrants', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('rooms', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('books', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('entrants', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('rooms', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'books', relatedCollection: 'entrants', type: Database::RELATION_MANY_TO_ONE, twoWay: true, id: 'entrant' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'entrants', relatedCollection: 'rooms', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $this->getDatabase()->createDocument('books', new Document([ + static::getDatabase()->createDocument('books', new Document([ '$id' => 'book1', '$permissions' => [ Permission::read(Role::any()), @@ -11263,7 +11303,7 @@ public function testNestedManyToOne_ManyToManyRelationship(): void ], ])); - $book1 = $this->getDatabase()->getDocument('books', 'book1'); + $book1 = static::getDatabase()->getDocument('books', 'book1'); $this->assertEquals('entrant1', $book1['entrant']['$id']); $this->assertArrayNotHasKey('books', $book1['entrant']); $this->assertEquals(2, \count($book1['entrant']['rooms'])); @@ -11273,26 +11313,26 @@ public function testNestedManyToOne_ManyToManyRelationship(): void public function testNestedManyToMany_OneToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('stones'); - $this->getDatabase()->createCollection('hearths'); - $this->getDatabase()->createCollection('plots'); + static::getDatabase()->createCollection('stones'); + static::getDatabase()->createCollection('hearths'); + static::getDatabase()->createCollection('plots'); - $this->getDatabase()->createAttribute('stones', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('hearths', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('plots', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('stones', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('hearths', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('plots', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'stones', relatedCollection: 'hearths', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'hearths', relatedCollection: 'plots', type: Database::RELATION_ONE_TO_ONE, @@ -11301,7 +11341,7 @@ public function testNestedManyToMany_OneToOneRelationship(): void twoWayKey: 'hearth' ); - $this->getDatabase()->createDocument('stones', new Document([ + static::getDatabase()->createDocument('stones', new Document([ '$id' => 'stone1', '$permissions' => [ Permission::read(Role::any()), @@ -11339,7 +11379,7 @@ public function testNestedManyToMany_OneToOneRelationship(): void ], ])); - $stone1 = $this->getDatabase()->getDocument('stones', 'stone1'); + $stone1 = static::getDatabase()->getDocument('stones', 'stone1'); $this->assertEquals(2, \count($stone1['hearths'])); $this->assertEquals('hearth1', $stone1['hearths'][0]['$id']); $this->assertEquals('hearth2', $stone1['hearths'][1]['$id']); @@ -11348,7 +11388,7 @@ public function testNestedManyToMany_OneToOneRelationship(): void $this->assertEquals('plot2', $stone1['hearths'][1]['plot']['$id']); $this->assertArrayNotHasKey('hearth', $stone1['hearths'][0]['plot']); - $this->getDatabase()->createDocument('plots', new Document([ + static::getDatabase()->createDocument('plots', new Document([ '$id' => 'plot3', '$permissions' => [ Permission::read(Role::any()), @@ -11372,7 +11412,7 @@ public function testNestedManyToMany_OneToOneRelationship(): void ], ])); - $plot3 = $this->getDatabase()->getDocument('plots', 'plot3'); + $plot3 = static::getDatabase()->getDocument('plots', 'plot3'); $this->assertEquals('hearth3', $plot3['hearth']['$id']); $this->assertArrayNotHasKey('plot', $plot3['hearth']); $this->assertEquals('stone2', $plot3['hearth']['stones'][0]['$id']); @@ -11381,26 +11421,26 @@ public function testNestedManyToMany_OneToOneRelationship(): void public function testNestedManyToMany_OneToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('groups'); - $this->getDatabase()->createCollection('tounaments'); - $this->getDatabase()->createCollection('prizes'); + static::getDatabase()->createCollection('groups'); + static::getDatabase()->createCollection('tounaments'); + static::getDatabase()->createCollection('prizes'); - $this->getDatabase()->createAttribute('groups', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('tounaments', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('prizes', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('groups', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('tounaments', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('prizes', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'groups', relatedCollection: 'tounaments', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'tounaments', relatedCollection: 'prizes', type: Database::RELATION_ONE_TO_MANY, @@ -11465,7 +11505,7 @@ public function testNestedManyToMany_OneToManyRelationship(): void ], ])); - $group1 = $this->getDatabase()->getDocument('groups', 'group1'); + $group1 = static::getDatabase()->getDocument('groups', 'group1'); $this->assertEquals(2, \count($group1['tounaments'])); $this->assertEquals('tounament1', $group1['tounaments'][0]['$id']); $this->assertEquals('tounament2', $group1['tounaments'][1]['$id']); @@ -11478,26 +11518,26 @@ public function testNestedManyToMany_OneToManyRelationship(): void public function testNestedManyToMany_ManyToOneRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('platforms'); - $this->getDatabase()->createCollection('games'); - $this->getDatabase()->createCollection('publishers'); + static::getDatabase()->createCollection('platforms'); + static::getDatabase()->createCollection('games'); + static::getDatabase()->createCollection('publishers'); - $this->getDatabase()->createAttribute('platforms', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('games', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('publishers', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('platforms', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('games', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('publishers', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'platforms', relatedCollection: 'games', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'games', relatedCollection: 'publishers', type: Database::RELATION_MANY_TO_ONE, @@ -11506,7 +11546,7 @@ public function testNestedManyToMany_ManyToOneRelationship(): void twoWayKey: 'games' ); - $this->getDatabase()->createDocument('platforms', new Document([ + static::getDatabase()->createDocument('platforms', new Document([ '$id' => 'platform1', '$permissions' => [ Permission::read(Role::any()), @@ -11544,7 +11584,7 @@ public function testNestedManyToMany_ManyToOneRelationship(): void ] ])); - $platform1 = $this->getDatabase()->getDocument('platforms', 'platform1'); + $platform1 = static::getDatabase()->getDocument('platforms', 'platform1'); $this->assertEquals(2, \count($platform1['games'])); $this->assertEquals('game1', $platform1['games'][0]['$id']); $this->assertEquals('game2', $platform1['games'][1]['$id']); @@ -11553,7 +11593,7 @@ public function testNestedManyToMany_ManyToOneRelationship(): void $this->assertEquals('publisher2', $platform1['games'][1]['publisher']['$id']); $this->assertArrayNotHasKey('games', $platform1['games'][0]['publisher']); - $this->getDatabase()->createDocument('publishers', new Document([ + static::getDatabase()->createDocument('publishers', new Document([ '$id' => 'publisher3', '$permissions' => [ Permission::read(Role::any()), @@ -11579,7 +11619,7 @@ public function testNestedManyToMany_ManyToOneRelationship(): void ], ])); - $publisher3 = $this->getDatabase()->getDocument('publishers', 'publisher3'); + $publisher3 = static::getDatabase()->getDocument('publishers', 'publisher3'); $this->assertEquals(1, \count($publisher3['games'])); $this->assertEquals('game3', $publisher3['games'][0]['$id']); $this->assertArrayNotHasKey('publisher', $publisher3['games'][0]); @@ -11589,26 +11629,26 @@ public function testNestedManyToMany_ManyToOneRelationship(): void public function testNestedManyToMany_ManyToManyRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('sauces'); - $this->getDatabase()->createCollection('pizzas'); - $this->getDatabase()->createCollection('toppings'); + static::getDatabase()->createCollection('sauces'); + static::getDatabase()->createCollection('pizzas'); + static::getDatabase()->createCollection('toppings'); - $this->getDatabase()->createAttribute('sauces', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('pizzas', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('toppings', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('sauces', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('pizzas', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('toppings', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'sauces', relatedCollection: 'pizzas', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'pizzas', relatedCollection: 'toppings', type: Database::RELATION_MANY_TO_MANY, @@ -11617,7 +11657,7 @@ public function testNestedManyToMany_ManyToManyRelationship(): void twoWayKey: 'pizzas' ); - $this->getDatabase()->createDocument('sauces', new Document([ + static::getDatabase()->createDocument('sauces', new Document([ '$id' => 'sauce1', '$permissions' => [ Permission::read(Role::any()), @@ -11673,7 +11713,7 @@ public function testNestedManyToMany_ManyToManyRelationship(): void ] ])); - $sauce1 = $this->getDatabase()->getDocument('sauces', 'sauce1'); + $sauce1 = static::getDatabase()->getDocument('sauces', 'sauce1'); $this->assertEquals(2, \count($sauce1['pizzas'])); $this->assertEquals('pizza1', $sauce1['pizzas'][0]['$id']); $this->assertEquals('pizza2', $sauce1['pizzas'][1]['$id']); @@ -11690,20 +11730,20 @@ public function testNestedManyToMany_ManyToManyRelationship(): void public function testInheritRelationshipPermissions(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('lawns', permissions: [Permission::create(Role::any())], documentSecurity: true); - $this->getDatabase()->createCollection('trees', permissions: [Permission::create(Role::any())], documentSecurity: true); - $this->getDatabase()->createCollection('birds', permissions: [Permission::create(Role::any())], documentSecurity: true); + static::getDatabase()->createCollection('lawns', permissions: [Permission::create(Role::any())], documentSecurity: true); + static::getDatabase()->createCollection('trees', permissions: [Permission::create(Role::any())], documentSecurity: true); + static::getDatabase()->createCollection('birds', permissions: [Permission::create(Role::any())], documentSecurity: true); - $this->getDatabase()->createAttribute('lawns', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('trees', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('birds', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('lawns', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('trees', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('birds', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'lawns', relatedCollection: 'trees', type: Database::RELATION_ONE_TO_MANY, @@ -11711,7 +11751,7 @@ public function testInheritRelationshipPermissions(): void twoWayKey: 'lawn', onDelete: Database::RELATION_MUTATE_CASCADE, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'trees', relatedCollection: 'birds', type: Database::RELATION_MANY_TO_MANY, @@ -11726,7 +11766,7 @@ public function testInheritRelationshipPermissions(): void Permission::delete(Role::user('user2')), ]; - $this->getDatabase()->createDocument('lawns', new Document([ + static::getDatabase()->createDocument('lawns', new Document([ '$id' => 'lawn1', '$permissions' => $permissions, 'name' => 'Lawn 1', @@ -11748,13 +11788,13 @@ public function testInheritRelationshipPermissions(): void ], ])); - $lawn1 = $this->getDatabase()->getDocument('lawns', 'lawn1'); + $lawn1 = static::getDatabase()->getDocument('lawns', 'lawn1'); $this->assertEquals($permissions, $lawn1->getPermissions()); $this->assertEquals($permissions, $lawn1['trees'][0]->getPermissions()); $this->assertEquals($permissions, $lawn1['trees'][0]['birds'][0]->getPermissions()); $this->assertEquals($permissions, $lawn1['trees'][0]['birds'][1]->getPermissions()); - $tree1 = $this->getDatabase()->getDocument('trees', 'tree1'); + $tree1 = static::getDatabase()->getDocument('trees', 'tree1'); $this->assertEquals($permissions, $tree1->getPermissions()); $this->assertEquals($permissions, $tree1['lawn']->getPermissions()); $this->assertEquals($permissions, $tree1['birds'][0]->getPermissions()); @@ -11766,18 +11806,18 @@ public function testInheritRelationshipPermissions(): void */ public function testEnforceRelationshipPermissions(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); - $lawn1 = $this->getDatabase()->getDocument('lawns', 'lawn1'); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + $lawn1 = static::getDatabase()->getDocument('lawns', 'lawn1'); $this->assertEquals('Lawn 1', $lawn1['name']); // Try update root document try { - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'lawns', $lawn1->getId(), $lawn1->setAttribute('name', 'Lawn 1 Updated') @@ -11789,7 +11829,7 @@ public function testEnforceRelationshipPermissions(): void // Try delete root document try { - $this->getDatabase()->deleteDocument( + static::getDatabase()->deleteDocument( 'lawns', $lawn1->getId(), ); @@ -11798,11 +11838,11 @@ public function testEnforceRelationshipPermissions(): void $this->assertEquals('Missing "delete" permission for role "user:user2". Only "["any"]" scopes are allowed and "["user:user2"]" was given.', $e->getMessage()); } - $tree1 = $this->getDatabase()->getDocument('trees', 'tree1'); + $tree1 = static::getDatabase()->getDocument('trees', 'tree1'); // Try update nested document try { - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'trees', $tree1->getId(), $tree1->setAttribute('name', 'Tree 1 Updated') @@ -11814,7 +11854,7 @@ public function testEnforceRelationshipPermissions(): void // Try delete nested document try { - $this->getDatabase()->deleteDocument( + static::getDatabase()->deleteDocument( 'trees', $tree1->getId(), ); @@ -11823,11 +11863,11 @@ public function testEnforceRelationshipPermissions(): void $this->assertEquals('Missing "delete" permission for role "user:user2". Only "["any"]" scopes are allowed and "["user:user2"]" was given.', $e->getMessage()); } - $bird1 = $this->getDatabase()->getDocument('birds', 'bird1'); + $bird1 = static::getDatabase()->getDocument('birds', 'bird1'); // Try update multi-level nested document try { - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( 'birds', $bird1->getId(), $bird1->setAttribute('name', 'Bird 1 Updated') @@ -11839,7 +11879,7 @@ public function testEnforceRelationshipPermissions(): void // Try delete multi-level nested document try { - $this->getDatabase()->deleteDocument( + static::getDatabase()->deleteDocument( 'birds', $bird1->getId(), ); @@ -11848,12 +11888,12 @@ public function testEnforceRelationshipPermissions(): void $this->assertEquals('Missing "delete" permission for role "user:user2". Only "["any"]" scopes are allowed and "["user:user2"]" was given.', $e->getMessage()); } - self::$authorization->addRole(Role::user('user1')->toString()); + Authorization::setRole(Role::user('user1')->toString()); - $bird1 = $this->getDatabase()->getDocument('birds', 'bird1'); + $bird1 = static::getDatabase()->getDocument('birds', 'bird1'); // Try update multi-level nested document - $bird1 = $this->getDatabase()->updateDocument( + $bird1 = static::getDatabase()->updateDocument( 'birds', $bird1->getId(), $bird1->setAttribute('name', 'Bird 1 Updated') @@ -11861,20 +11901,20 @@ public function testEnforceRelationshipPermissions(): void $this->assertEquals('Bird 1 Updated', $bird1['name']); - self::$authorization->addRole(Role::user('user2')->toString()); + Authorization::setRole(Role::user('user2')->toString()); // Try delete multi-level nested document - $deleted = $this->getDatabase()->deleteDocument( + $deleted = static::getDatabase()->deleteDocument( 'birds', $bird1->getId(), ); $this->assertEquals(true, $deleted); - $tree1 = $this->getDatabase()->getDocument('trees', 'tree1'); + $tree1 = static::getDatabase()->getDocument('trees', 'tree1'); $this->assertEquals(1, count($tree1['birds'])); // Try update nested document - $tree1 = $this->getDatabase()->updateDocument( + $tree1 = static::getDatabase()->updateDocument( 'trees', $tree1->getId(), $tree1->setAttribute('name', 'Tree 1 Updated') @@ -11883,17 +11923,17 @@ public function testEnforceRelationshipPermissions(): void $this->assertEquals('Tree 1 Updated', $tree1['name']); // Try delete nested document - $deleted = $this->getDatabase()->deleteDocument( + $deleted = static::getDatabase()->deleteDocument( 'trees', $tree1->getId(), ); $this->assertEquals(true, $deleted); - $lawn1 = $this->getDatabase()->getDocument('lawns', 'lawn1'); + $lawn1 = static::getDatabase()->getDocument('lawns', 'lawn1'); $this->assertEquals(0, count($lawn1['trees'])); // Create document with no permissions - $this->getDatabase()->createDocument('lawns', new Document([ + static::getDatabase()->createDocument('lawns', new Document([ '$id' => 'lawn2', 'name' => 'Lawn 2', 'trees' => [ @@ -11910,19 +11950,19 @@ public function testEnforceRelationshipPermissions(): void ], ])); - $lawn2 = $this->getDatabase()->getDocument('lawns', 'lawn2'); + $lawn2 = static::getDatabase()->getDocument('lawns', 'lawn2'); $this->assertEquals(true, $lawn2->isEmpty()); - $tree2 = $this->getDatabase()->getDocument('trees', 'tree2'); + $tree2 = static::getDatabase()->getDocument('trees', 'tree2'); $this->assertEquals(true, $tree2->isEmpty()); - $bird3 = $this->getDatabase()->getDocument('birds', 'bird3'); + $bird3 = static::getDatabase()->getDocument('birds', 'bird3'); $this->assertEquals(true, $bird3->isEmpty()); } public function testExceedMaxDepthOneToMany(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -11932,24 +11972,24 @@ public function testExceedMaxDepthOneToMany(): void $level3Collection = 'level3OneToMany'; $level4Collection = 'level4OneToMany'; - $this->getDatabase()->createCollection($level1Collection); - $this->getDatabase()->createCollection($level2Collection); - $this->getDatabase()->createCollection($level3Collection); - $this->getDatabase()->createCollection($level4Collection); + static::getDatabase()->createCollection($level1Collection); + static::getDatabase()->createCollection($level2Collection); + static::getDatabase()->createCollection($level3Collection); + static::getDatabase()->createCollection($level4Collection); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level1Collection, relatedCollection: $level2Collection, type: Database::RELATION_ONE_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level2Collection, relatedCollection: $level3Collection, type: Database::RELATION_ONE_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level3Collection, relatedCollection: $level4Collection, type: Database::RELATION_ONE_TO_MANY, @@ -11957,7 +11997,7 @@ public function testExceedMaxDepthOneToMany(): void ); // Exceed create depth - $level1 = $this->getDatabase()->createDocument($level1Collection, new Document([ + $level1 = static::getDatabase()->createDocument($level1Collection, new Document([ '$id' => 'level1', '$permissions' => [ Permission::read(Role::any()), @@ -11986,13 +12026,13 @@ public function testExceedMaxDepthOneToMany(): void $this->assertArrayNotHasKey('level4', $level1[$level2Collection][0][$level3Collection][0]); // Make sure level 4 document was not created - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertEquals(0, count($level3[$level4Collection])); - $level4 = $this->getDatabase()->getDocument($level4Collection, 'level4'); + $level4 = static::getDatabase()->getDocument($level4Collection, 'level4'); $this->assertTrue($level4->isEmpty()); // Exceed fetch depth - $level1 = $this->getDatabase()->getDocument($level1Collection, 'level1'); + $level1 = static::getDatabase()->getDocument($level1Collection, 'level1'); $this->assertEquals(1, count($level1[$level2Collection])); $this->assertEquals('level2', $level1[$level2Collection][0]->getId()); $this->assertEquals(1, count($level1[$level2Collection][0][$level3Collection])); @@ -12001,7 +12041,7 @@ public function testExceedMaxDepthOneToMany(): void // Exceed update depth - $level1 = $this->getDatabase()->updateDocument( + $level1 = static::getDatabase()->updateDocument( $level1Collection, 'level1', $level1 @@ -12026,15 +12066,15 @@ public function testExceedMaxDepthOneToMany(): void $this->assertArrayNotHasKey($level4Collection, $level1[$level2Collection][0][$level3Collection][0]); // Make sure level 4 document was not created - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3new'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3new'); $this->assertEquals(0, count($level3[$level4Collection])); - $level4 = $this->getDatabase()->getDocument($level4Collection, 'level4new'); + $level4 = static::getDatabase()->getDocument($level4Collection, 'level4new'); $this->assertTrue($level4->isEmpty()); } public function testExceedMaxDepthOneToOne(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12044,24 +12084,24 @@ public function testExceedMaxDepthOneToOne(): void $level3Collection = 'level3OneToOne'; $level4Collection = 'level4OneToOne'; - $this->getDatabase()->createCollection($level1Collection); - $this->getDatabase()->createCollection($level2Collection); - $this->getDatabase()->createCollection($level3Collection); - $this->getDatabase()->createCollection($level4Collection); + static::getDatabase()->createCollection($level1Collection); + static::getDatabase()->createCollection($level2Collection); + static::getDatabase()->createCollection($level3Collection); + static::getDatabase()->createCollection($level4Collection); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level1Collection, relatedCollection: $level2Collection, type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level2Collection, relatedCollection: $level3Collection, type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level3Collection, relatedCollection: $level4Collection, type: Database::RELATION_ONE_TO_ONE, @@ -12069,7 +12109,7 @@ public function testExceedMaxDepthOneToOne(): void ); // Exceed create depth - $level1 = $this->getDatabase()->createDocument($level1Collection, new Document([ + $level1 = static::getDatabase()->createDocument($level1Collection, new Document([ '$id' => 'level1', '$permissions' => [ Permission::read(Role::any()), @@ -12092,18 +12132,18 @@ public function testExceedMaxDepthOneToOne(): void $this->assertArrayNotHasKey($level4Collection, $level1[$level2Collection][$level3Collection]); // Confirm the 4th level document does not exist - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertNull($level3[$level4Collection]); // Create level 4 document $level3->setAttribute($level4Collection, new Document([ '$id' => 'level4', ])); - $level3 = $this->getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); + $level3 = static::getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); $this->assertEquals('level4', $level3[$level4Collection]->getId()); // Exceed fetch depth - $level1 = $this->getDatabase()->getDocument($level1Collection, 'level1'); + $level1 = static::getDatabase()->getDocument($level1Collection, 'level1'); $this->assertArrayHasKey($level2Collection, $level1); $this->assertEquals('level2', $level1[$level2Collection]->getId()); $this->assertArrayHasKey($level3Collection, $level1[$level2Collection]); @@ -12113,7 +12153,7 @@ public function testExceedMaxDepthOneToOne(): void public function testExceedMaxDepthOneToOneNull(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12123,31 +12163,31 @@ public function testExceedMaxDepthOneToOneNull(): void $level3Collection = 'level3OneToOneNull'; $level4Collection = 'level4OneToOneNull'; - $this->getDatabase()->createCollection($level1Collection); - $this->getDatabase()->createCollection($level2Collection); - $this->getDatabase()->createCollection($level3Collection); - $this->getDatabase()->createCollection($level4Collection); + static::getDatabase()->createCollection($level1Collection); + static::getDatabase()->createCollection($level2Collection); + static::getDatabase()->createCollection($level3Collection); + static::getDatabase()->createCollection($level4Collection); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level1Collection, relatedCollection: $level2Collection, type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level2Collection, relatedCollection: $level3Collection, type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level3Collection, relatedCollection: $level4Collection, type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $level1 = $this->getDatabase()->createDocument($level1Collection, new Document([ + $level1 = static::getDatabase()->createDocument($level1Collection, new Document([ '$id' => 'level1', '$permissions' => [ Permission::read(Role::any()), @@ -12170,20 +12210,20 @@ public function testExceedMaxDepthOneToOneNull(): void $this->assertArrayNotHasKey($level4Collection, $level1[$level2Collection][$level3Collection]); // Confirm the 4th level document does not exist - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertNull($level3[$level4Collection]); // Create level 4 document $level3->setAttribute($level4Collection, new Document([ '$id' => 'level4', ])); - $level3 = $this->getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); + $level3 = static::getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); $this->assertEquals('level4', $level3[$level4Collection]->getId()); - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertEquals('level4', $level3[$level4Collection]->getId()); // Exceed fetch depth - $level1 = $this->getDatabase()->getDocument($level1Collection, 'level1'); + $level1 = static::getDatabase()->getDocument($level1Collection, 'level1'); $this->assertArrayHasKey($level2Collection, $level1); $this->assertEquals('level2', $level1[$level2Collection]->getId()); $this->assertArrayHasKey($level3Collection, $level1[$level2Collection]); @@ -12193,7 +12233,7 @@ public function testExceedMaxDepthOneToOneNull(): void public function testExceedMaxDepthManyToOneParent(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12203,31 +12243,31 @@ public function testExceedMaxDepthManyToOneParent(): void $level3Collection = 'level3ManyToOneParent'; $level4Collection = 'level4ManyToOneParent'; - $this->getDatabase()->createCollection($level1Collection); - $this->getDatabase()->createCollection($level2Collection); - $this->getDatabase()->createCollection($level3Collection); - $this->getDatabase()->createCollection($level4Collection); + static::getDatabase()->createCollection($level1Collection); + static::getDatabase()->createCollection($level2Collection); + static::getDatabase()->createCollection($level3Collection); + static::getDatabase()->createCollection($level4Collection); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level1Collection, relatedCollection: $level2Collection, type: Database::RELATION_MANY_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level2Collection, relatedCollection: $level3Collection, type: Database::RELATION_MANY_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level3Collection, relatedCollection: $level4Collection, type: Database::RELATION_MANY_TO_ONE, twoWay: true, ); - $level1 = $this->getDatabase()->createDocument($level1Collection, new Document([ + $level1 = static::getDatabase()->createDocument($level1Collection, new Document([ '$id' => 'level1', '$permissions' => [ Permission::read(Role::any()), @@ -12250,20 +12290,20 @@ public function testExceedMaxDepthManyToOneParent(): void $this->assertArrayNotHasKey($level4Collection, $level1[$level2Collection][$level3Collection]); // Confirm the 4th level document does not exist - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertNull($level3[$level4Collection]); // Create level 4 document $level3->setAttribute($level4Collection, new Document([ '$id' => 'level4', ])); - $level3 = $this->getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); + $level3 = static::getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); $this->assertEquals('level4', $level3[$level4Collection]->getId()); - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertEquals('level4', $level3[$level4Collection]->getId()); // Exceed fetch depth - $level1 = $this->getDatabase()->getDocument($level1Collection, 'level1'); + $level1 = static::getDatabase()->getDocument($level1Collection, 'level1'); $this->assertArrayHasKey($level2Collection, $level1); $this->assertEquals('level2', $level1[$level2Collection]->getId()); $this->assertArrayHasKey($level3Collection, $level1[$level2Collection]); @@ -12273,7 +12313,7 @@ public function testExceedMaxDepthManyToOneParent(): void public function testExceedMaxDepthOneToManyChild(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12283,31 +12323,31 @@ public function testExceedMaxDepthOneToManyChild(): void $level3Collection = 'level3OneToManyChild'; $level4Collection = 'level4OneToManyChild'; - $this->getDatabase()->createCollection($level1Collection); - $this->getDatabase()->createCollection($level2Collection); - $this->getDatabase()->createCollection($level3Collection); - $this->getDatabase()->createCollection($level4Collection); + static::getDatabase()->createCollection($level1Collection); + static::getDatabase()->createCollection($level2Collection); + static::getDatabase()->createCollection($level3Collection); + static::getDatabase()->createCollection($level4Collection); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level1Collection, relatedCollection: $level2Collection, type: Database::RELATION_ONE_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level2Collection, relatedCollection: $level3Collection, type: Database::RELATION_ONE_TO_MANY, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: $level3Collection, relatedCollection: $level4Collection, type: Database::RELATION_ONE_TO_MANY, twoWay: true, ); - $level1 = $this->getDatabase()->createDocument($level1Collection, new Document([ + $level1 = static::getDatabase()->createDocument($level1Collection, new Document([ '$id' => 'level1', '$permissions' => [ Permission::read(Role::any()), @@ -12336,23 +12376,23 @@ public function testExceedMaxDepthOneToManyChild(): void $this->assertArrayNotHasKey($level4Collection, $level1[$level2Collection][0][$level3Collection][0]); // Confirm the 4th level document does not exist - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertEquals(0, count($level3[$level4Collection])); // Create level 4 document $level3->setAttribute($level4Collection, [new Document([ '$id' => 'level4', ])]); - $level3 = $this->getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); + $level3 = static::getDatabase()->updateDocument($level3Collection, $level3->getId(), $level3); $this->assertEquals('level4', $level3[$level4Collection][0]->getId()); // Verify level 4 document is set - $level3 = $this->getDatabase()->getDocument($level3Collection, 'level3'); + $level3 = static::getDatabase()->getDocument($level3Collection, 'level3'); $this->assertArrayHasKey($level4Collection, $level3); $this->assertEquals('level4', $level3[$level4Collection][0]->getId()); // Exceed fetch depth - $level4 = $this->getDatabase()->getDocument($level4Collection, 'level4'); + $level4 = static::getDatabase()->getDocument($level4Collection, 'level4'); $this->assertArrayHasKey($level3Collection, $level4); $this->assertEquals('level3', $level4[$level3Collection]->getId()); $this->assertArrayHasKey($level2Collection, $level4[$level3Collection]); @@ -12362,7 +12402,7 @@ public function testExceedMaxDepthOneToManyChild(): void public function testCreateRelationshipMissingCollection(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12370,7 +12410,7 @@ public function testCreateRelationshipMissingCollection(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Collection not found'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'missing', relatedCollection: 'missing', type: Database::RELATION_ONE_TO_MANY, @@ -12380,17 +12420,17 @@ public function testCreateRelationshipMissingCollection(): void public function testCreateRelationshipMissingRelatedCollection(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('test'); + static::getDatabase()->createCollection('test'); $this->expectException(Exception::class); $this->expectExceptionMessage('Related collection not found'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'test', relatedCollection: 'missing', type: Database::RELATION_ONE_TO_MANY, @@ -12400,15 +12440,15 @@ public function testCreateRelationshipMissingRelatedCollection(): void public function testCreateDuplicateRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('test1'); - $this->getDatabase()->createCollection('test2'); + static::getDatabase()->createCollection('test1'); + static::getDatabase()->createCollection('test2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'test1', relatedCollection: 'test2', type: Database::RELATION_ONE_TO_MANY, @@ -12418,7 +12458,7 @@ public function testCreateDuplicateRelationship(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Attribute already exists'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'test1', relatedCollection: 'test2', type: Database::RELATION_ONE_TO_MANY, @@ -12428,18 +12468,18 @@ public function testCreateDuplicateRelationship(): void public function testCreateInvalidRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('test3'); - $this->getDatabase()->createCollection('test4'); + static::getDatabase()->createCollection('test3'); + static::getDatabase()->createCollection('test4'); $this->expectException(Exception::class); $this->expectExceptionMessage('Invalid relationship type'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'test3', relatedCollection: 'test4', type: 'invalid', @@ -12504,7 +12544,7 @@ public function testOneToManyAndManyToOneDeleteRelationship(): void public function testDeleteMissingRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12512,20 +12552,20 @@ public function testDeleteMissingRelationship(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Attribute not found'); - $this->getDatabase()->deleteRelationship('test', 'test2'); + static::getDatabase()->deleteRelationship('test', 'test2'); } public function testCreateInvalidIntValueRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('invalid1'); - $this->getDatabase()->createCollection('invalid2'); + static::getDatabase()->createCollection('invalid1'); + static::getDatabase()->createCollection('invalid2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'invalid1', relatedCollection: 'invalid2', type: Database::RELATION_ONE_TO_ONE, @@ -12535,7 +12575,7 @@ public function testCreateInvalidIntValueRelationship(): void $this->expectException(RelationshipException::class); $this->expectExceptionMessage('Invalid relationship value. Must be either a document, document ID, or an array of documents or document IDs.'); - $this->getDatabase()->createDocument('invalid1', new Document([ + static::getDatabase()->createDocument('invalid1', new Document([ '$id' => ID::unique(), 'invalid2' => 10, ])); @@ -12546,7 +12586,7 @@ public function testCreateInvalidIntValueRelationship(): void */ public function testCreateInvalidObjectValueRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -12554,7 +12594,7 @@ public function testCreateInvalidObjectValueRelationship(): void $this->expectException(RelationshipException::class); $this->expectExceptionMessage('Invalid relationship value. Must be either a document, document ID, or an array of documents or document IDs.'); - $this->getDatabase()->createDocument('invalid1', new Document([ + static::getDatabase()->createDocument('invalid1', new Document([ '$id' => ID::unique(), 'invalid2' => new \stdClass(), ])); @@ -12565,12 +12605,12 @@ public function testCreateInvalidObjectValueRelationship(): void */ public function testCreateInvalidArrayIntValueRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'invalid1', relatedCollection: 'invalid2', type: Database::RELATION_ONE_TO_MANY, @@ -12582,7 +12622,7 @@ public function testCreateInvalidArrayIntValueRelationship(): void $this->expectException(RelationshipException::class); $this->expectExceptionMessage('Invalid relationship value. Must be either a document, document ID, or an array of documents or document IDs.'); - $this->getDatabase()->createDocument('invalid1', new Document([ + static::getDatabase()->createDocument('invalid1', new Document([ '$id' => ID::unique(), 'invalid3' => [10], ])); @@ -12590,21 +12630,21 @@ public function testCreateInvalidArrayIntValueRelationship(): void public function testCreateEmptyValueRelationship(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('null1'); - $this->getDatabase()->createCollection('null2'); + static::getDatabase()->createCollection('null1'); + static::getDatabase()->createCollection('null2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'null1', relatedCollection: 'null2', type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'null1', relatedCollection: 'null2', type: Database::RELATION_ONE_TO_MANY, @@ -12612,7 +12652,7 @@ public function testCreateEmptyValueRelationship(): void id: 'null3', twoWayKey: 'null4', ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'null1', relatedCollection: 'null2', type: Database::RELATION_MANY_TO_ONE, @@ -12620,7 +12660,7 @@ public function testCreateEmptyValueRelationship(): void id: 'null4', twoWayKey: 'null5', ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'null1', relatedCollection: 'null2', type: Database::RELATION_MANY_TO_MANY, @@ -12629,21 +12669,21 @@ public function testCreateEmptyValueRelationship(): void twoWayKey: 'null7', ); - $document = $this->getDatabase()->createDocument('null1', new Document([ + $document = static::getDatabase()->createDocument('null1', new Document([ '$id' => ID::unique(), 'null2' => null, ])); $this->assertEquals(null, $document->getAttribute('null2')); - $document = $this->getDatabase()->createDocument('null2', new Document([ + $document = static::getDatabase()->createDocument('null2', new Document([ '$id' => ID::unique(), 'null1' => null, ])); $this->assertEquals(null, $document->getAttribute('null1')); - $document = $this->getDatabase()->createDocument('null1', new Document([ + $document = static::getDatabase()->createDocument('null1', new Document([ '$id' => ID::unique(), 'null3' => null, ])); @@ -12651,35 +12691,35 @@ public function testCreateEmptyValueRelationship(): void // One to many will be empty array instead of null $this->assertEquals([], $document->getAttribute('null3')); - $document = $this->getDatabase()->createDocument('null2', new Document([ + $document = static::getDatabase()->createDocument('null2', new Document([ '$id' => ID::unique(), 'null4' => null, ])); $this->assertEquals(null, $document->getAttribute('null4')); - $document = $this->getDatabase()->createDocument('null1', new Document([ + $document = static::getDatabase()->createDocument('null1', new Document([ '$id' => ID::unique(), 'null4' => null, ])); $this->assertEquals(null, $document->getAttribute('null4')); - $document = $this->getDatabase()->createDocument('null2', new Document([ + $document = static::getDatabase()->createDocument('null2', new Document([ '$id' => ID::unique(), 'null5' => null, ])); $this->assertEquals([], $document->getAttribute('null5')); - $document = $this->getDatabase()->createDocument('null1', new Document([ + $document = static::getDatabase()->createDocument('null1', new Document([ '$id' => ID::unique(), 'null6' => null, ])); $this->assertEquals([], $document->getAttribute('null6')); - $document = $this->getDatabase()->createDocument('null2', new Document([ + $document = static::getDatabase()->createDocument('null2', new Document([ '$id' => ID::unique(), 'null7' => null, ])); @@ -12689,15 +12729,15 @@ public function testCreateEmptyValueRelationship(): void public function testDeleteCollectionDeletesRelationships(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('testers'); - $this->getDatabase()->createCollection('devices'); + static::getDatabase()->createCollection('testers'); + static::getDatabase()->createCollection('devices'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'testers', relatedCollection: 'devices', type: Database::RELATION_ONE_TO_MANY, @@ -12705,17 +12745,17 @@ public function testDeleteCollectionDeletesRelationships(): void twoWayKey: 'tester' ); - $testers = $this->getDatabase()->getCollection('testers'); - $devices = $this->getDatabase()->getCollection('devices'); + $testers = static::getDatabase()->getCollection('testers'); + $devices = static::getDatabase()->getCollection('devices'); $this->assertEquals(1, \count($testers->getAttribute('attributes'))); $this->assertEquals(1, \count($devices->getAttribute('attributes'))); $this->assertEquals(1, \count($devices->getAttribute('indexes'))); - $this->getDatabase()->deleteCollection('testers'); + static::getDatabase()->deleteCollection('testers'); - $testers = $this->getDatabase()->getCollection('testers'); - $devices = $this->getDatabase()->getCollection('devices'); + $testers = static::getDatabase()->getCollection('testers'); + $devices = static::getDatabase()->getCollection('devices'); $this->assertEquals(true, $testers->isEmpty()); $this->assertEquals(0, \count($devices->getAttribute('attributes'))); @@ -12724,15 +12764,15 @@ public function testDeleteCollectionDeletesRelationships(): void public function testDeleteTwoWayRelationshipFromChild(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('drivers'); - $this->getDatabase()->createCollection('licenses'); + static::getDatabase()->createCollection('drivers'); + static::getDatabase()->createCollection('licenses'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'drivers', relatedCollection: 'licenses', type: Database::RELATION_ONE_TO_ONE, @@ -12741,25 +12781,25 @@ public function testDeleteTwoWayRelationshipFromChild(): void twoWayKey: 'driver' ); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); $this->assertEquals(1, \count($drivers->getAttribute('attributes'))); $this->assertEquals(1, \count($drivers->getAttribute('indexes'))); $this->assertEquals(1, \count($licenses->getAttribute('attributes'))); $this->assertEquals(1, \count($licenses->getAttribute('indexes'))); - $this->getDatabase()->deleteRelationship('licenses', 'driver'); + static::getDatabase()->deleteRelationship('licenses', 'driver'); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); $this->assertEquals(0, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); $this->assertEquals(0, \count($licenses->getAttribute('attributes'))); $this->assertEquals(0, \count($licenses->getAttribute('indexes'))); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'drivers', relatedCollection: 'licenses', type: Database::RELATION_ONE_TO_MANY, @@ -12768,25 +12808,25 @@ public function testDeleteTwoWayRelationshipFromChild(): void twoWayKey: 'driver' ); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); $this->assertEquals(1, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); $this->assertEquals(1, \count($licenses->getAttribute('attributes'))); $this->assertEquals(1, \count($licenses->getAttribute('indexes'))); - $this->getDatabase()->deleteRelationship('licenses', 'driver'); + static::getDatabase()->deleteRelationship('licenses', 'driver'); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); $this->assertEquals(0, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); $this->assertEquals(0, \count($licenses->getAttribute('attributes'))); $this->assertEquals(0, \count($licenses->getAttribute('indexes'))); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'licenses', relatedCollection: 'drivers', type: Database::RELATION_MANY_TO_ONE, @@ -12795,25 +12835,25 @@ public function testDeleteTwoWayRelationshipFromChild(): void twoWayKey: 'licenses' ); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); $this->assertEquals(1, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); $this->assertEquals(1, \count($licenses->getAttribute('attributes'))); $this->assertEquals(1, \count($licenses->getAttribute('indexes'))); - $this->getDatabase()->deleteRelationship('drivers', 'licenses'); + static::getDatabase()->deleteRelationship('drivers', 'licenses'); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); $this->assertEquals(0, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); $this->assertEquals(0, \count($licenses->getAttribute('attributes'))); $this->assertEquals(0, \count($licenses->getAttribute('indexes'))); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'licenses', relatedCollection: 'drivers', type: Database::RELATION_MANY_TO_MANY, @@ -12822,9 +12862,9 @@ public function testDeleteTwoWayRelationshipFromChild(): void twoWayKey: 'licenses' ); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); - $junction = $this->getDatabase()->getCollection('_' . $licenses->getInternalId() . '_' . $drivers->getInternalId()); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); + $junction = static::getDatabase()->getCollection('_' . $licenses->getInternalId() . '_' . $drivers->getInternalId()); $this->assertEquals(1, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); @@ -12833,11 +12873,11 @@ public function testDeleteTwoWayRelationshipFromChild(): void $this->assertEquals(2, \count($junction->getAttribute('attributes'))); $this->assertEquals(2, \count($junction->getAttribute('indexes'))); - $this->getDatabase()->deleteRelationship('drivers', 'licenses'); + static::getDatabase()->deleteRelationship('drivers', 'licenses'); - $drivers = $this->getDatabase()->getCollection('drivers'); - $licenses = $this->getDatabase()->getCollection('licenses'); - $junction = $this->getDatabase()->getCollection('_licenses_drivers'); + $drivers = static::getDatabase()->getCollection('drivers'); + $licenses = static::getDatabase()->getCollection('licenses'); + $junction = static::getDatabase()->getCollection('_licenses_drivers'); $this->assertEquals(0, \count($drivers->getAttribute('attributes'))); $this->assertEquals(0, \count($drivers->getAttribute('indexes'))); @@ -12849,20 +12889,20 @@ public function testDeleteTwoWayRelationshipFromChild(): void public function testUpdateRelationshipToExistingKey(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('ovens'); - $this->getDatabase()->createCollection('cakes'); + static::getDatabase()->createCollection('ovens'); + static::getDatabase()->createCollection('cakes'); - $this->getDatabase()->createAttribute('ovens', 'maxTemp', Database::VAR_INTEGER, 0, true); - $this->getDatabase()->createAttribute('ovens', 'owner', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('cakes', 'height', Database::VAR_INTEGER, 0, true); - $this->getDatabase()->createAttribute('cakes', 'colour', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('ovens', 'maxTemp', Database::VAR_INTEGER, 0, true); + static::getDatabase()->createAttribute('ovens', 'owner', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('cakes', 'height', Database::VAR_INTEGER, 0, true); + static::getDatabase()->createAttribute('cakes', 'colour', Database::VAR_STRING, 255, true); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'ovens', relatedCollection: 'cakes', type: Database::RELATION_ONE_TO_MANY, @@ -12872,14 +12912,14 @@ public function testUpdateRelationshipToExistingKey(): void ); try { - $this->getDatabase()->updateRelationship('ovens', 'cakes', newKey: 'owner'); + static::getDatabase()->updateRelationship('ovens', 'cakes', newKey: 'owner'); $this->fail('Failed to throw exception'); } catch (DuplicateException $e) { $this->assertEquals('Attribute already exists', $e->getMessage()); } try { - $this->getDatabase()->updateRelationship('ovens', 'cakes', newTwoWayKey: 'height'); + static::getDatabase()->updateRelationship('ovens', 'cakes', newTwoWayKey: 'height'); $this->fail('Failed to throw exception'); } catch (DuplicateException $e) { $this->assertEquals('Related attribute already exists', $e->getMessage()); @@ -12888,29 +12928,29 @@ public function testUpdateRelationshipToExistingKey(): void public function testOneToOneRelationshipKeyWithSymbols(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('$symbols_coll.ection1'); - $this->getDatabase()->createCollection('$symbols_coll.ection2'); + static::getDatabase()->createCollection('$symbols_coll.ection1'); + static::getDatabase()->createCollection('$symbols_coll.ection2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: '$symbols_coll.ection1', relatedCollection: '$symbols_coll.ection2', type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $doc1 = $this->getDatabase()->createDocument('$symbols_coll.ection2', new Document([ + $doc1 = static::getDatabase()->createDocument('$symbols_coll.ection2', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()) ] ])); - $doc2 = $this->getDatabase()->createDocument('$symbols_coll.ection1', new Document([ + $doc2 = static::getDatabase()->createDocument('$symbols_coll.ection1', new Document([ '$id' => ID::unique(), '$symbols_coll.ection2' => $doc1->getId(), '$permissions' => [ @@ -12919,8 +12959,8 @@ public function testOneToOneRelationshipKeyWithSymbols(): void ] ])); - $doc1 = $this->getDatabase()->getDocument('$symbols_coll.ection2', $doc1->getId()); - $doc2 = $this->getDatabase()->getDocument('$symbols_coll.ection1', $doc2->getId()); + $doc1 = static::getDatabase()->getDocument('$symbols_coll.ection2', $doc1->getId()); + $doc2 = static::getDatabase()->getDocument('$symbols_coll.ection1', $doc2->getId()); $this->assertEquals($doc2->getId(), $doc1->getAttribute('$symbols_coll.ection1')->getId()); $this->assertEquals($doc1->getId(), $doc2->getAttribute('$symbols_coll.ection2')->getId()); @@ -12928,29 +12968,29 @@ public function testOneToOneRelationshipKeyWithSymbols(): void public function testOneToManyRelationshipKeyWithSymbols(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('$symbols_coll.ection3'); - $this->getDatabase()->createCollection('$symbols_coll.ection4'); + static::getDatabase()->createCollection('$symbols_coll.ection3'); + static::getDatabase()->createCollection('$symbols_coll.ection4'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: '$symbols_coll.ection3', relatedCollection: '$symbols_coll.ection4', type: Database::RELATION_ONE_TO_MANY, twoWay: true, ); - $doc1 = $this->getDatabase()->createDocument('$symbols_coll.ection4', new Document([ + $doc1 = static::getDatabase()->createDocument('$symbols_coll.ection4', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()) ] ])); - $doc2 = $this->getDatabase()->createDocument('$symbols_coll.ection3', new Document([ + $doc2 = static::getDatabase()->createDocument('$symbols_coll.ection3', new Document([ '$id' => ID::unique(), '$symbols_coll.ection4' => [$doc1->getId()], '$permissions' => [ @@ -12959,8 +12999,8 @@ public function testOneToManyRelationshipKeyWithSymbols(): void ] ])); - $doc1 = $this->getDatabase()->getDocument('$symbols_coll.ection4', $doc1->getId()); - $doc2 = $this->getDatabase()->getDocument('$symbols_coll.ection3', $doc2->getId()); + $doc1 = static::getDatabase()->getDocument('$symbols_coll.ection4', $doc1->getId()); + $doc2 = static::getDatabase()->getDocument('$symbols_coll.ection3', $doc2->getId()); $this->assertEquals($doc2->getId(), $doc1->getAttribute('$symbols_coll.ection3')->getId()); $this->assertEquals($doc1->getId(), $doc2->getAttribute('$symbols_coll.ection4')[0]->getId()); @@ -12968,29 +13008,29 @@ public function testOneToManyRelationshipKeyWithSymbols(): void public function testManyToOneRelationshipKeyWithSymbols(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('$symbols_coll.ection5'); - $this->getDatabase()->createCollection('$symbols_coll.ection6'); + static::getDatabase()->createCollection('$symbols_coll.ection5'); + static::getDatabase()->createCollection('$symbols_coll.ection6'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: '$symbols_coll.ection5', relatedCollection: '$symbols_coll.ection6', type: Database::RELATION_MANY_TO_ONE, twoWay: true, ); - $doc1 = $this->getDatabase()->createDocument('$symbols_coll.ection6', new Document([ + $doc1 = static::getDatabase()->createDocument('$symbols_coll.ection6', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()) ] ])); - $doc2 = $this->getDatabase()->createDocument('$symbols_coll.ection5', new Document([ + $doc2 = static::getDatabase()->createDocument('$symbols_coll.ection5', new Document([ '$id' => ID::unique(), '$symbols_coll.ection6' => $doc1->getId(), '$permissions' => [ @@ -12999,8 +13039,8 @@ public function testManyToOneRelationshipKeyWithSymbols(): void ] ])); - $doc1 = $this->getDatabase()->getDocument('$symbols_coll.ection6', $doc1->getId()); - $doc2 = $this->getDatabase()->getDocument('$symbols_coll.ection5', $doc2->getId()); + $doc1 = static::getDatabase()->getDocument('$symbols_coll.ection6', $doc1->getId()); + $doc2 = static::getDatabase()->getDocument('$symbols_coll.ection5', $doc2->getId()); $this->assertEquals($doc2->getId(), $doc1->getAttribute('$symbols_coll.ection5')[0]->getId()); $this->assertEquals($doc1->getId(), $doc2->getAttribute('$symbols_coll.ection6')->getId()); @@ -13008,29 +13048,29 @@ public function testManyToOneRelationshipKeyWithSymbols(): void public function testManyToManyRelationshipKeyWithSymbols(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('$symbols_coll.ection7'); - $this->getDatabase()->createCollection('$symbols_coll.ection8'); + static::getDatabase()->createCollection('$symbols_coll.ection7'); + static::getDatabase()->createCollection('$symbols_coll.ection8'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: '$symbols_coll.ection7', relatedCollection: '$symbols_coll.ection8', type: Database::RELATION_MANY_TO_MANY, twoWay: true, ); - $doc1 = $this->getDatabase()->createDocument('$symbols_coll.ection8', new Document([ + $doc1 = static::getDatabase()->createDocument('$symbols_coll.ection8', new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()) ] ])); - $doc2 = $this->getDatabase()->createDocument('$symbols_coll.ection7', new Document([ + $doc2 = static::getDatabase()->createDocument('$symbols_coll.ection7', new Document([ '$id' => ID::unique(), '$symbols_coll.ection8' => [$doc1->getId()], '$permissions' => [ @@ -13039,8 +13079,8 @@ public function testManyToManyRelationshipKeyWithSymbols(): void ] ])); - $doc1 = $this->getDatabase()->getDocument('$symbols_coll.ection8', $doc1->getId()); - $doc2 = $this->getDatabase()->getDocument('$symbols_coll.ection7', $doc2->getId()); + $doc1 = static::getDatabase()->getDocument('$symbols_coll.ection8', $doc1->getId()); + $doc2 = static::getDatabase()->getDocument('$symbols_coll.ection7', $doc2->getId()); $this->assertEquals($doc2->getId(), $doc1->getAttribute('$symbols_coll.ection7')[0]->getId()); $this->assertEquals($doc1->getId(), $doc2->getAttribute('$symbols_coll.ection8')[0]->getId()); @@ -13048,16 +13088,16 @@ public function testManyToManyRelationshipKeyWithSymbols(): void public function testCascadeMultiDelete(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('cascadeMultiDelete1'); - $this->getDatabase()->createCollection('cascadeMultiDelete2'); - $this->getDatabase()->createCollection('cascadeMultiDelete3'); + static::getDatabase()->createCollection('cascadeMultiDelete1'); + static::getDatabase()->createCollection('cascadeMultiDelete2'); + static::getDatabase()->createCollection('cascadeMultiDelete3'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'cascadeMultiDelete1', relatedCollection: 'cascadeMultiDelete2', type: Database::RELATION_ONE_TO_MANY, @@ -13065,7 +13105,7 @@ public function testCascadeMultiDelete(): void onDelete: Database::RELATION_MUTATE_CASCADE ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'cascadeMultiDelete2', relatedCollection: 'cascadeMultiDelete3', type: Database::RELATION_ONE_TO_MANY, @@ -13073,7 +13113,7 @@ public function testCascadeMultiDelete(): void onDelete: Database::RELATION_MUTATE_CASCADE ); - $root = $this->getDatabase()->createDocument('cascadeMultiDelete1', new Document([ + $root = static::getDatabase()->createDocument('cascadeMultiDelete1', new Document([ '$id' => 'cascadeMultiDelete1', '$permissions' => [ Permission::read(Role::any()), @@ -13102,18 +13142,18 @@ public function testCascadeMultiDelete(): void $this->assertCount(1, $root->getAttribute('cascadeMultiDelete2')); $this->assertCount(1, $root->getAttribute('cascadeMultiDelete2')[0]->getAttribute('cascadeMultiDelete3')); - $this->assertEquals(true, $this->getDatabase()->deleteDocument('cascadeMultiDelete1', $root->getId())); + $this->assertEquals(true, static::getDatabase()->deleteDocument('cascadeMultiDelete1', $root->getId())); - $multi2 = $this->getDatabase()->getDocument('cascadeMultiDelete2', 'cascadeMultiDelete2'); + $multi2 = static::getDatabase()->getDocument('cascadeMultiDelete2', 'cascadeMultiDelete2'); $this->assertEquals(true, $multi2->isEmpty()); - $multi3 = $this->getDatabase()->getDocument('cascadeMultiDelete3', 'cascadeMultiDelete3'); + $multi3 = static::getDatabase()->getDocument('cascadeMultiDelete3', 'cascadeMultiDelete3'); $this->assertEquals(true, $multi3->isEmpty()); } public function testCollectionUpdate(): Document { - $collection = $this->getDatabase()->createCollection('collectionUpdate', permissions: [ + $collection = static::getDatabase()->createCollection('collectionUpdate', permissions: [ Permission::create(Role::users()), Permission::read(Role::users()), Permission::update(Role::users()), @@ -13122,19 +13162,19 @@ public function testCollectionUpdate(): Document $this->assertInstanceOf(Document::class, $collection); - $collection = $this->getDatabase()->getCollection('collectionUpdate'); + $collection = static::getDatabase()->getCollection('collectionUpdate'); $this->assertFalse($collection->getAttribute('documentSecurity')); $this->assertIsArray($collection->getPermissions()); $this->assertCount(4, $collection->getPermissions()); - $collection = $this->getDatabase()->updateCollection('collectionUpdate', [], true); + $collection = static::getDatabase()->updateCollection('collectionUpdate', [], true); $this->assertTrue($collection->getAttribute('documentSecurity')); $this->assertIsArray($collection->getPermissions()); $this->assertEmpty($collection->getPermissions()); - $collection = $this->getDatabase()->getCollection('collectionUpdate'); + $collection = static::getDatabase()->getCollection('collectionUpdate'); $this->assertTrue($collection->getAttribute('documentSecurity')); $this->assertIsArray($collection->getPermissions()); @@ -13149,14 +13189,14 @@ public function testCollectionUpdate(): Document public function testCollectionUpdatePermissionsThrowException(Document $collection): void { $this->expectException(DatabaseException::class); - $this->getDatabase()->updateCollection($collection->getId(), permissions: [ + static::getDatabase()->updateCollection($collection->getId(), permissions: [ 'i dont work' ], documentSecurity: false); } public function testCollectionPermissions(): Document { - $collection = $this->getDatabase()->createCollection('collectionSecurity', permissions: [ + $collection = static::getDatabase()->createCollection('collectionSecurity', permissions: [ Permission::create(Role::users()), Permission::read(Role::users()), Permission::update(Role::users()), @@ -13165,7 +13205,7 @@ public function testCollectionPermissions(): Document $this->assertInstanceOf(Document::class, $collection); - $this->assertTrue($this->getDatabase()->createAttribute( + $this->assertTrue(static::getDatabase()->createAttribute( collection: $collection->getId(), id: 'test', type: Database::VAR_STRING, @@ -13179,7 +13219,7 @@ public function testCollectionPermissions(): Document public function testCollectionPermissionsExceptions(): void { $this->expectException(DatabaseException::class); - $this->getDatabase()->createCollection('collectionSecurity', permissions: [ + static::getDatabase()->createCollection('collectionSecurity', permissions: [ 'i dont work' ]); } @@ -13190,10 +13230,10 @@ public function testCollectionPermissionsExceptions(): void */ public function testCollectionPermissionsCreateWorks(Document $collection): array { - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $document = $this->getDatabase()->createDocument($collection->getId(), new Document([ + $document = static::getDatabase()->createDocument($collection->getId(), new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::user('random')), @@ -13213,11 +13253,11 @@ public function testCollectionPermissionsCreateWorks(Document $collection): arra */ public function testCollectionPermissionsCreateThrowsException(Document $collection): void { - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $this->getDatabase()->createDocument($collection->getId(), new Document([ + static::getDatabase()->createDocument($collection->getId(), new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), @@ -13237,10 +13277,10 @@ public function testCollectionPermissionsGetWorks(array $data): array { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $document = $this->getDatabase()->getDocument( + $document = static::getDatabase()->getDocument( $collection->getId(), $document->getId() ); @@ -13258,10 +13298,10 @@ public function testCollectionPermissionsGetThrowsException(array $data): void { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); - $document = $this->getDatabase()->getDocument( + $document = static::getDatabase()->getDocument( $collection->getId(), $document->getId(), ); @@ -13278,17 +13318,17 @@ public function testCollectionPermissionsFindWorks(array $data): array { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $documents = $this->getDatabase()->find($collection->getId()); + $documents = static::getDatabase()->find($collection->getId()); $this->assertNotEmpty($documents); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('random')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('random')->toString()); try { - $this->getDatabase()->find($collection->getId()); + static::getDatabase()->find($collection->getId()); $this->fail('Failed to throw exception'); } catch (AuthorizationException) { } @@ -13304,11 +13344,11 @@ public function testCollectionPermissionsFindThrowsException(array $data): void { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $this->getDatabase()->find($collection->getId()); + static::getDatabase()->find($collection->getId()); } /** @@ -13320,10 +13360,10 @@ public function testCollectionPermissionsCountWorks(array $data): array { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $count = $this->getDatabase()->count( + $count = static::getDatabase()->count( $collection->getId() ); @@ -13340,10 +13380,10 @@ public function testCollectionPermissionsCountThrowsException(array $data): void { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); - $count = $this->getDatabase()->count( + $count = static::getDatabase()->count( $collection->getId() ); $this->assertEmpty($count); @@ -13358,10 +13398,10 @@ public function testCollectionPermissionsUpdateWorks(array $data): array { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $this->assertInstanceOf(Document::class, $this->getDatabase()->updateDocument( + $this->assertInstanceOf(Document::class, static::getDatabase()->updateDocument( $collection->getId(), $document->getId(), $document->setAttribute('test', 'ipsum') @@ -13377,11 +13417,11 @@ public function testCollectionPermissionsUpdateWorks(array $data): array public function testCollectionPermissionsUpdateThrowsException(array $data): void { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $document = $this->getDatabase()->updateDocument( + $document = static::getDatabase()->updateDocument( $collection->getId(), $document->getId(), $document->setAttribute('test', 'lorem') @@ -13396,11 +13436,11 @@ public function testCollectionPermissionsDeleteThrowsException(array $data): voi { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $this->getDatabase()->deleteDocument( + static::getDatabase()->deleteDocument( $collection->getId(), $document->getId() ); @@ -13414,10 +13454,10 @@ public function testCollectionPermissionsDeleteWorks(array $data): void { [$collection, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $this->assertTrue($this->getDatabase()->deleteDocument( + $this->assertTrue(static::getDatabase()->deleteDocument( $collection->getId(), $document->getId() )); @@ -13428,7 +13468,7 @@ public function testCollectionPermissionsDeleteWorks(array $data): void */ public function testCollectionPermissionsRelationships(): array { - $collection = $this->getDatabase()->createCollection('collectionSecurity.Parent', permissions: [ + $collection = static::getDatabase()->createCollection('collectionSecurity.Parent', permissions: [ Permission::create(Role::users()), Permission::read(Role::users()), Permission::update(Role::users()), @@ -13437,7 +13477,7 @@ public function testCollectionPermissionsRelationships(): array $this->assertInstanceOf(Document::class, $collection); - $this->assertTrue($this->getDatabase()->createAttribute( + $this->assertTrue(static::getDatabase()->createAttribute( collection: $collection->getId(), id: 'test', type: Database::VAR_STRING, @@ -13445,7 +13485,7 @@ public function testCollectionPermissionsRelationships(): array required: false )); - $collectionOneToOne = $this->getDatabase()->createCollection('collectionSecurity.OneToOne', permissions: [ + $collectionOneToOne = static::getDatabase()->createCollection('collectionSecurity.OneToOne', permissions: [ Permission::create(Role::users()), Permission::read(Role::users()), Permission::update(Role::users()), @@ -13454,7 +13494,7 @@ public function testCollectionPermissionsRelationships(): array $this->assertInstanceOf(Document::class, $collectionOneToOne); - $this->assertTrue($this->getDatabase()->createAttribute( + $this->assertTrue(static::getDatabase()->createAttribute( collection: $collectionOneToOne->getId(), id: 'test', type: Database::VAR_STRING, @@ -13462,7 +13502,7 @@ public function testCollectionPermissionsRelationships(): array required: false )); - $this->assertTrue($this->getDatabase()->createRelationship( + $this->assertTrue(static::getDatabase()->createRelationship( collection: $collection->getId(), relatedCollection: $collectionOneToOne->getId(), type: Database::RELATION_ONE_TO_ONE, @@ -13470,7 +13510,7 @@ public function testCollectionPermissionsRelationships(): array onDelete: Database::RELATION_MUTATE_CASCADE )); - $collectionOneToMany = $this->getDatabase()->createCollection('collectionSecurity.OneToMany', permissions: [ + $collectionOneToMany = static::getDatabase()->createCollection('collectionSecurity.OneToMany', permissions: [ Permission::create(Role::users()), Permission::read(Role::users()), Permission::update(Role::users()), @@ -13479,7 +13519,7 @@ public function testCollectionPermissionsRelationships(): array $this->assertInstanceOf(Document::class, $collectionOneToMany); - $this->assertTrue($this->getDatabase()->createAttribute( + $this->assertTrue(static::getDatabase()->createAttribute( collection: $collectionOneToMany->getId(), id: 'test', type: Database::VAR_STRING, @@ -13487,7 +13527,7 @@ public function testCollectionPermissionsRelationships(): array required: false )); - $this->assertTrue($this->getDatabase()->createRelationship( + $this->assertTrue(static::getDatabase()->createRelationship( collection: $collection->getId(), relatedCollection: $collectionOneToMany->getId(), type: Database::RELATION_ONE_TO_MANY, @@ -13506,10 +13546,10 @@ public function testCollectionPermissionsRelationships(): array public function testCollectionPermissionsRelationshipsCreateWorks(array $data): array { [$collection, $collectionOneToOne, $collectionOneToMany] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $document = $this->getDatabase()->createDocument($collection->getId(), new Document([ + $document = static::getDatabase()->createDocument($collection->getId(), new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::user('random')), @@ -13559,11 +13599,11 @@ public function testCollectionPermissionsRelationshipsCreateThrowsException(arra { [$collection, $collectionOneToOne, $collectionOneToMany] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $this->getDatabase()->createDocument($collection->getId(), new Document([ + static::getDatabase()->createDocument($collection->getId(), new Document([ '$id' => ID::unique(), '$permissions' => [ Permission::read(Role::any()), @@ -13582,10 +13622,10 @@ public function testCollectionPermissionsRelationshipsGetWorks(array $data): arr { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $document = $this->getDatabase()->getDocument( + $document = static::getDatabase()->getDocument( $collection->getId(), $document->getId() ); @@ -13596,10 +13636,10 @@ public function testCollectionPermissionsRelationshipsGetWorks(array $data): arr $this->assertCount(2, $document->getAttribute(Database::RELATION_ONE_TO_MANY)); $this->assertFalse($document->isEmpty()); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('random')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('random')->toString()); - $document = $this->getDatabase()->getDocument( + $document = static::getDatabase()->getDocument( $collection->getId(), $document->getId() ); @@ -13621,10 +13661,10 @@ public function testCollectionPermissionsRelationshipsGetThrowsException(array $ { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); - $document = $this->getDatabase()->getDocument( + $document = static::getDatabase()->getDocument( $collection->getId(), $document->getId(), ); @@ -13640,10 +13680,10 @@ public function testCollectionPermissionsRelationshipsFindWorks(array $data): vo { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $documents = $this->getDatabase()->find( + $documents = static::getDatabase()->find( $collection->getId() ); @@ -13656,10 +13696,10 @@ public function testCollectionPermissionsRelationshipsFindWorks(array $data): vo $this->assertCount(2, $document->getAttribute(Database::RELATION_ONE_TO_MANY)); $this->assertFalse($document->isEmpty()); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('random')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('random')->toString()); - $documents = $this->getDatabase()->find( + $documents = static::getDatabase()->find( $collection->getId() ); @@ -13672,10 +13712,10 @@ public function testCollectionPermissionsRelationshipsFindWorks(array $data): vo $this->assertCount(1, $document->getAttribute(Database::RELATION_ONE_TO_MANY)); $this->assertFalse($document->isEmpty()); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('unknown')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('unknown')->toString()); - $documents = $this->getDatabase()->find( + $documents = static::getDatabase()->find( $collection->getId() ); @@ -13691,28 +13731,28 @@ public function testCollectionPermissionsRelationshipsCountWorks(array $data): v { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $documents = $this->getDatabase()->count( + $documents = static::getDatabase()->count( $collection->getId() ); $this->assertEquals(1, $documents); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('random')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('random')->toString()); - $documents = $this->getDatabase()->count( + $documents = static::getDatabase()->count( $collection->getId() ); $this->assertEquals(1, $documents); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('unknown')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('unknown')->toString()); - $documents = $this->getDatabase()->count( + $documents = static::getDatabase()->count( $collection->getId() ); @@ -13728,9 +13768,9 @@ public function testCollectionPermissionsRelationshipsUpdateWorks(array $data): { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); - $this->getDatabase()->updateDocument( + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); + static::getDatabase()->updateDocument( $collection->getId(), $document->getId(), $document @@ -13738,10 +13778,10 @@ public function testCollectionPermissionsRelationshipsUpdateWorks(array $data): $this->assertTrue(true); - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('random')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('random')->toString()); - $this->getDatabase()->updateDocument( + static::getDatabase()->updateDocument( $collection->getId(), $document->getId(), $document->setAttribute('test', 'ipsum') @@ -13760,11 +13800,11 @@ public function testCollectionPermissionsRelationshipsUpdateThrowsException(arra { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $document = $this->getDatabase()->updateDocument( + $document = static::getDatabase()->updateDocument( $collection->getId(), $document->getId(), $document->setAttribute('test', $document->getAttribute('test').'new_value') @@ -13779,11 +13819,11 @@ public function testCollectionPermissionsRelationshipsDeleteThrowsException(arra { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); $this->expectException(AuthorizationException::class); - $document = $this->getDatabase()->deleteDocument( + $document = static::getDatabase()->deleteDocument( $collection->getId(), $document->getId() ); @@ -13797,10 +13837,10 @@ public function testCollectionPermissionsRelationshipsDeleteWorks(array $data): { [$collection, $collectionOneToOne, $collectionOneToMany, $document] = $data; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::users()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::users()->toString()); - $this->assertTrue($this->getDatabase()->deleteDocument( + $this->assertTrue(static::getDatabase()->deleteDocument( $collection->getId(), $document->getId() )); @@ -13808,28 +13848,28 @@ public function testCollectionPermissionsRelationshipsDeleteWorks(array $data): public function testCreateRelationDocumentWithoutUpdatePermission(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::user('a')->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::user('a')->toString()); - $this->getDatabase()->createCollection('parentRelationTest', [], [], [ + static::getDatabase()->createCollection('parentRelationTest', [], [], [ Permission::read(Role::user('a')), Permission::create(Role::user('a')), Permission::update(Role::user('a')), Permission::delete(Role::user('a')) ]); - $this->getDatabase()->createCollection('childRelationTest', [], [], [ + static::getDatabase()->createCollection('childRelationTest', [], [], [ Permission::create(Role::user('a')), Permission::read(Role::user('a')), ]); - $this->getDatabase()->createAttribute('parentRelationTest', 'name', Database::VAR_STRING, 255, false); - $this->getDatabase()->createAttribute('childRelationTest', 'name', Database::VAR_STRING, 255, false); + static::getDatabase()->createAttribute('parentRelationTest', 'name', Database::VAR_STRING, 255, false); + static::getDatabase()->createAttribute('childRelationTest', 'name', Database::VAR_STRING, 255, false); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'parentRelationTest', relatedCollection: 'childRelationTest', type: Database::RELATION_ONE_TO_MANY, @@ -13837,7 +13877,7 @@ public function testCreateRelationDocumentWithoutUpdatePermission(): void ); // Create document with relationship with nested data - $parent = $this->getDatabase()->createDocument('parentRelationTest', new Document([ + $parent = static::getDatabase()->createDocument('parentRelationTest', new Document([ '$id' => 'parent1', 'name' => 'Parent 1', 'children' => [ @@ -13853,21 +13893,21 @@ public function testCreateRelationDocumentWithoutUpdatePermission(): void '$id' => 'child2', ], ]); - $updatedParent = $this->getDatabase()->updateDocument('parentRelationTest', 'parent1', $parent); + $updatedParent = static::getDatabase()->updateDocument('parentRelationTest', 'parent1', $parent); $this->assertEquals('child2', $updatedParent->getAttribute('children')[0]->getId()); - $this->getDatabase()->deleteCollection('parentRelationTest'); - $this->getDatabase()->deleteCollection('childRelationTest'); + static::getDatabase()->deleteCollection('parentRelationTest'); + static::getDatabase()->deleteCollection('childRelationTest'); } public function testUpdateDocumentWithRelationships(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } - $this->getDatabase()->createCollection('userProfiles', [ + static::getDatabase()->createCollection('userProfiles', [ new Document([ '$id' => ID::custom('username'), 'type' => Database::VAR_STRING, @@ -13885,7 +13925,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('links', [ + static::getDatabase()->createCollection('links', [ new Document([ '$id' => ID::custom('title'), 'type' => Database::VAR_STRING, @@ -13903,7 +13943,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('videos', [ + static::getDatabase()->createCollection('videos', [ new Document([ '$id' => ID::custom('title'), 'type' => Database::VAR_STRING, @@ -13921,7 +13961,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('products', [ + static::getDatabase()->createCollection('products', [ new Document([ '$id' => ID::custom('title'), 'type' => Database::VAR_STRING, @@ -13939,7 +13979,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('settings', [ + static::getDatabase()->createCollection('settings', [ new Document([ '$id' => ID::custom('metaTitle'), 'type' => Database::VAR_STRING, @@ -13957,7 +13997,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('appearance', [ + static::getDatabase()->createCollection('appearance', [ new Document([ '$id' => ID::custom('metaTitle'), 'type' => Database::VAR_STRING, @@ -13975,7 +14015,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('group', [ + static::getDatabase()->createCollection('group', [ new Document([ '$id' => ID::custom('name'), 'type' => Database::VAR_STRING, @@ -13993,7 +14033,7 @@ public function testUpdateDocumentWithRelationships(): void Permission::update(Role::any()), Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('community', [ + static::getDatabase()->createCollection('community', [ new Document([ '$id' => ID::custom('name'), 'type' => Database::VAR_STRING, @@ -14012,21 +14052,21 @@ public function testUpdateDocumentWithRelationships(): void Permission::delete(Role::any()) ]); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'links', type: Database::RELATION_ONE_TO_MANY, id: 'links' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'videos', type: Database::RELATION_ONE_TO_MANY, id: 'videos' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'products', type: Database::RELATION_ONE_TO_MANY, @@ -14035,35 +14075,35 @@ public function testUpdateDocumentWithRelationships(): void twoWayKey: 'userProfile', ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'settings', type: Database::RELATION_ONE_TO_ONE, id: 'settings' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'appearance', type: Database::RELATION_ONE_TO_ONE, id: 'appearance' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'group', type: Database::RELATION_MANY_TO_ONE, id: 'group' ); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'userProfiles', relatedCollection: 'community', type: Database::RELATION_MANY_TO_ONE, id: 'community' ); - $profile = $this->getDatabase()->createDocument('userProfiles', new Document([ + $profile = static::getDatabase()->createDocument('userProfiles', new Document([ '$id' => '1', 'username' => 'user1', 'links' => [ @@ -14126,7 +14166,7 @@ public function testUpdateDocumentWithRelationships(): void 'name' => 'New Group Name', ]); - $updatedProfile = $this->getDatabase()->updateDocument('userProfiles', '1', $profile); + $updatedProfile = static::getDatabase()->updateDocument('userProfiles', '1', $profile); $this->assertEquals('New Link Value', $updatedProfile->getAttribute('links')[0]->getAttribute('title')); $this->assertEquals('New Meta Title', $updatedProfile->getAttribute('settings')->getAttribute('metaTitle')); @@ -14139,25 +14179,25 @@ public function testUpdateDocumentWithRelationships(): void $this->assertEquals('Community 1', $updatedProfile->getAttribute('community')->getAttribute('name')); // updating document using two way key in one to many relationship - $product = $this->getDatabase()->getDocument('products', 'product1'); + $product = static::getDatabase()->getDocument('products', 'product1'); $product->setAttribute('userProfile', [ '$id' => '1', 'username' => 'updated user value', ]); - $updatedProduct = $this->getDatabase()->updateDocument('products', 'product1', $product); + $updatedProduct = static::getDatabase()->updateDocument('products', 'product1', $product); $this->assertEquals('updated user value', $updatedProduct->getAttribute('userProfile')->getAttribute('username')); $this->assertEquals('Product 1', $updatedProduct->getAttribute('title')); $this->assertEquals('product1', $updatedProduct->getId()); $this->assertEquals('1', $updatedProduct->getAttribute('userProfile')->getId()); - $this->getDatabase()->deleteCollection('userProfiles'); - $this->getDatabase()->deleteCollection('links'); - $this->getDatabase()->deleteCollection('settings'); - $this->getDatabase()->deleteCollection('group'); - $this->getDatabase()->deleteCollection('community'); - $this->getDatabase()->deleteCollection('videos'); - $this->getDatabase()->deleteCollection('products'); - $this->getDatabase()->deleteCollection('appearance'); + static::getDatabase()->deleteCollection('userProfiles'); + static::getDatabase()->deleteCollection('links'); + static::getDatabase()->deleteCollection('settings'); + static::getDatabase()->deleteCollection('group'); + static::getDatabase()->deleteCollection('community'); + static::getDatabase()->deleteCollection('videos'); + static::getDatabase()->deleteCollection('products'); + static::getDatabase()->deleteCollection('appearance'); } public function testRecreateOneToOneOneWayRelationshipFromParent(): void @@ -15186,12 +15226,12 @@ public function testRecreateManyToManyTwoWayRelationshipFromChild(): void public function testLabels(): void { - $this->assertInstanceOf('Utopia\Database\Document', $this->getDatabase()->createCollection( + $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection( 'labels_test', )); - $this->getDatabase()->createAttribute('labels_test', 'attr1', Database::VAR_STRING, 10, false); + static::getDatabase()->createAttribute('labels_test', 'attr1', Database::VAR_STRING, 10, false); - $this->getDatabase()->createDocument('labels_test', new Document([ + static::getDatabase()->createDocument('labels_test', new Document([ '$id' => 'doc1', 'attr1' => 'value1', '$permissions' => [ @@ -15199,20 +15239,20 @@ public function testLabels(): void ], ])); - $documents = $this->getDatabase()->find('labels_test'); + $documents = static::getDatabase()->find('labels_test'); $this->assertEmpty($documents); - self::$authorization->addRole(Role::label('reader')->toString()); + Authorization::setRole(Role::label('reader')->toString()); - $documents = $this->getDatabase()->find('labels_test'); + $documents = static::getDatabase()->find('labels_test'); $this->assertCount(1, $documents); } public function testEnableDisableValidation(): void { - $database = $this->getDatabase(); + $database = static::getDatabase(); $database->createCollection('validation', permissions: [ Permission::create(Role::any()), @@ -15269,21 +15309,21 @@ public function testEnableDisableValidation(): void public function testMetadata(): void { - $this->getDatabase()->setMetadata('key', 'value'); + static::getDatabase()->setMetadata('key', 'value'); - $this->getDatabase()->createCollection('testers'); + static::getDatabase()->createCollection('testers'); - $this->assertEquals(['key' => 'value'], $this->getDatabase()->getMetadata()); + $this->assertEquals(['key' => 'value'], static::getDatabase()->getMetadata()); - $this->getDatabase()->resetMetadata(); + static::getDatabase()->resetMetadata(); - $this->assertEquals([], $this->getDatabase()->getMetadata()); + $this->assertEquals([], static::getDatabase()->getMetadata()); } public function testEmptyOperatorValues(): void { try { - $this->getDatabase()->findOne('documents', [ + static::getDatabase()->findOne('documents', [ Query::equal('string', []), ]); $this->fail('Failed to throw exception'); @@ -15293,7 +15333,7 @@ public function testEmptyOperatorValues(): void } try { - $this->getDatabase()->findOne('documents', [ + static::getDatabase()->findOne('documents', [ Query::contains('string', []), ]); $this->fail('Failed to throw exception'); @@ -15317,7 +15357,7 @@ public function testSharedTables(): void /** * Default mode already tested, we'll test 'schema' and 'table' isolation here */ - $database = $this->getDatabase(); + $database = static::getDatabase(); if (!$database->getAdapter()->getSupportForSchemas()) { $this->expectNotToPerformAssertions(); @@ -15544,7 +15584,7 @@ public function testSharedTablesDuplicatesDontThrow(): void public function testTransformations(): void { - $this->getDatabase()->createCollection('docs', attributes: [ + static::getDatabase()->createCollection('docs', attributes: [ new Document([ '$id' => 'name', 'type' => Database::VAR_STRING, @@ -15553,16 +15593,16 @@ public function testTransformations(): void ]) ]); - $this->getDatabase()->createDocument('docs', new Document([ + static::getDatabase()->createDocument('docs', new Document([ '$id' => 'doc1', 'name' => 'value1', ])); - $this->getDatabase()->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $query) { + static::getDatabase()->before(Database::EVENT_DOCUMENT_READ, 'test', function (string $query) { return "SELECT 1"; }); - $result = $this->getDatabase()->getDocument('docs', 'doc1'); + $result = static::getDatabase()->getDocument('docs', 'doc1'); $this->assertTrue($result->isEmpty()); } @@ -16616,8 +16656,8 @@ public function testUpdateDocumentsRelationships(): void public function testEvents(): void { - self::$authorization->skip(function () { - $database = $this->getDatabase(); + Authorization::skip(function () { + $database = static::getDatabase(); $events = [ Database::EVENT_DATABASE_CREATE, diff --git a/tests/e2e/Adapter/MariaDBTest.php b/tests/e2e/Adapter/MariaDBTest.php index c60dfbed9..d7966e657 100644 --- a/tests/e2e/Adapter/MariaDBTest.php +++ b/tests/e2e/Adapter/MariaDBTest.php @@ -28,7 +28,7 @@ public static function getAdapterName(): string /** * @return Database */ - public function getDatabase(bool $fresh = false): Database + public static function getDatabase(bool $fresh = false): Database { if (!is_null(self::$database) && !$fresh) { return self::$database; @@ -46,9 +46,9 @@ public function getDatabase(bool $fresh = false): Database $cache = new Cache(new RedisAdapter($redis)); $database = new Database(new MariaDB($pdo), $cache); - $database->setAuthorization(self::$authorization); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + $database + ->setDatabase('utopiaTests') + ->setNamespace(static::$namespace = 'myapp_' . uniqid()); if ($database->exists()) { $database->delete(); diff --git a/tests/e2e/Adapter/MongoDBTest.php b/tests/e2e/Adapter/MongoDBTest.php index a17e595bd..3f3b5d5de 100644 --- a/tests/e2e/Adapter/MongoDBTest.php +++ b/tests/e2e/Adapter/MongoDBTest.php @@ -29,7 +29,7 @@ public static function getAdapterName(): string * @return Database * @throws Exception */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -51,9 +51,9 @@ public function getDatabase(): Database ); $database = new Database(new Mongo($client), $cache); - $database->setAuthorization(self::$authorization); - $database->setDatabase($schema); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + $database + ->setDatabase($schema) + ->setNamespace(static::$namespace = 'myapp_' . uniqid()); if ($database->exists()) { $database->delete(); @@ -70,10 +70,10 @@ public function getDatabase(): Database public function testCreateExistsDelete(): void { // Mongo creates databases on the fly, so exists would always pass. So we override this test to remove the exists check. - $this->assertNotNull($this->getDatabase()->create()); - $this->assertEquals(true, $this->getDatabase()->delete($this->testDatabase)); - $this->assertEquals(true, $this->getDatabase()->create()); - $this->assertEquals($this->getDatabase(), $this->getDatabase()->setDatabase($this->testDatabase)); + $this->assertNotNull(static::getDatabase()->create()); + $this->assertEquals(true, static::getDatabase()->delete($this->testDatabase)); + $this->assertEquals(true, static::getDatabase()->create()); + $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); } public function testRenameAttribute(): void diff --git a/tests/e2e/Adapter/MySQLTest.php b/tests/e2e/Adapter/MySQLTest.php index bae839a10..8d855d6ca 100644 --- a/tests/e2e/Adapter/MySQLTest.php +++ b/tests/e2e/Adapter/MySQLTest.php @@ -28,7 +28,7 @@ public static function getAdapterName(): string /** * @return Database */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -48,9 +48,9 @@ public function getDatabase(): Database $cache = new Cache(new RedisAdapter($redis)); $database = new Database(new MySQL($pdo), $cache); - $database->setAuthorization(self::$authorization); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + $database + ->setDatabase('utopiaTests') + ->setNamespace(static::$namespace = 'myapp_' . uniqid()); if ($database->exists()) { $database->delete(); diff --git a/tests/e2e/Adapter/PostgresTest.php b/tests/e2e/Adapter/PostgresTest.php index 19b972115..e024e4442 100644 --- a/tests/e2e/Adapter/PostgresTest.php +++ b/tests/e2e/Adapter/PostgresTest.php @@ -27,7 +27,7 @@ public static function getAdapterName(): string /** * @reture Adapter */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -45,9 +45,9 @@ public function getDatabase(): Database $cache = new Cache(new RedisAdapter($redis)); $database = new Database(new Postgres($pdo), $cache); - $database->setAuthorization(self::$authorization); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + $database + ->setDatabase('utopiaTests') + ->setNamespace(static::$namespace = 'myapp_' . uniqid()); if ($database->exists()) { $database->delete(); diff --git a/tests/e2e/Adapter/SQLiteTest.php b/tests/e2e/Adapter/SQLiteTest.php index bc903fd7d..2d4240994 100644 --- a/tests/e2e/Adapter/SQLiteTest.php +++ b/tests/e2e/Adapter/SQLiteTest.php @@ -28,7 +28,7 @@ public static function getAdapterName(): string /** * @return Database */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -51,9 +51,9 @@ public function getDatabase(): Database $cache = new Cache(new RedisAdapter($redis)); $database = new Database(new SQLite($pdo), $cache); - $database->setAuthorization(self::$authorization); - $database->setDatabase('utopiaTests'); - $database->setNamespace(static::$namespace = 'myapp_' . uniqid()); + $database + ->setDatabase('utopiaTests') + ->setNamespace(static::$namespace = 'myapp_' . uniqid()); if ($database->exists()) { $database->delete(); diff --git a/tests/e2e/Adapter/SharedTables/MariaDBTest.php b/tests/e2e/Adapter/SharedTables/MariaDBTest.php index 7ee04ac5b..249e13319 100644 --- a/tests/e2e/Adapter/SharedTables/MariaDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MariaDBTest.php @@ -29,7 +29,7 @@ public static function getAdapterName(): string /** * @return Database */ - public function getDatabase(bool $fresh = false): Database + public static function getDatabase(bool $fresh = false): Database { if (!is_null(self::$database) && !$fresh) { return self::$database; @@ -48,7 +48,6 @@ public function getDatabase(bool $fresh = false): Database $database = new Database(new MariaDB($pdo), $cache); $database - ->setAuthorization(self::$authorization) ->setDatabase('utopiaTests') ->setSharedTables(true) ->setTenant(999) diff --git a/tests/e2e/Adapter/SharedTables/MongoDBTest.php b/tests/e2e/Adapter/SharedTables/MongoDBTest.php index c5968727c..47f480073 100644 --- a/tests/e2e/Adapter/SharedTables/MongoDBTest.php +++ b/tests/e2e/Adapter/SharedTables/MongoDBTest.php @@ -30,7 +30,7 @@ public static function getAdapterName(): string * @return Database * @throws Exception */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -53,7 +53,6 @@ public function getDatabase(): Database $database = new Database(new Mongo($client), $cache); $database - ->setAuthorization(self::$authorization) ->setDatabase($schema) ->setSharedTables(true) ->setTenant(999) diff --git a/tests/e2e/Adapter/SharedTables/MySQLTest.php b/tests/e2e/Adapter/SharedTables/MySQLTest.php index af244d985..689ea49c7 100644 --- a/tests/e2e/Adapter/SharedTables/MySQLTest.php +++ b/tests/e2e/Adapter/SharedTables/MySQLTest.php @@ -29,7 +29,7 @@ public static function getAdapterName(): string /** * @return Database */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -50,7 +50,6 @@ public function getDatabase(): Database $database = new Database(new MySQL($pdo), $cache); $database - ->setAuthorization(self::$authorization) ->setDatabase('utopiaTests') ->setSharedTables(true) ->setTenant(999) diff --git a/tests/e2e/Adapter/SharedTables/PostgresTest.php b/tests/e2e/Adapter/SharedTables/PostgresTest.php index 4de48ee1d..df85dfeb0 100644 --- a/tests/e2e/Adapter/SharedTables/PostgresTest.php +++ b/tests/e2e/Adapter/SharedTables/PostgresTest.php @@ -28,7 +28,7 @@ public static function getAdapterName(): string /** * @reture Adapter */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -47,7 +47,6 @@ public function getDatabase(): Database $database = new Database(new Postgres($pdo), $cache); $database - ->setAuthorization(self::$authorization) ->setDatabase('utopiaTests') ->setSharedTables(true) ->setTenant(999) diff --git a/tests/e2e/Adapter/SharedTables/SQLiteTest.php b/tests/e2e/Adapter/SharedTables/SQLiteTest.php index a1254505d..a69984216 100644 --- a/tests/e2e/Adapter/SharedTables/SQLiteTest.php +++ b/tests/e2e/Adapter/SharedTables/SQLiteTest.php @@ -29,7 +29,7 @@ public static function getAdapterName(): string /** * @return Database */ - public function getDatabase(): Database + public static function getDatabase(): Database { if (!is_null(self::$database)) { return self::$database; @@ -53,7 +53,6 @@ public function getDatabase(): Database $database = new Database(new SQLite($pdo), $cache); $database - ->setAuthorization(self::$authorization) ->setDatabase('utopiaTests') ->setSharedTables(true) ->setTenant(999) diff --git a/tests/unit/Format.php b/tests/unit/Format.php index 92cf10f28..f4f4a4a0f 100644 --- a/tests/unit/Format.php +++ b/tests/unit/Format.php @@ -2,15 +2,14 @@ namespace Tests\Unit; -use Utopia\Http\Validator; -use Utopia\Http\Validator\Text; +use Utopia\Validator\Text; /** * Format Test for Email * * Validate that an variable is a valid email address * - * @package Utopia\Http\Validator + * @package Utopia\Validator */ class Format extends Text { diff --git a/tests/unit/Validator/AuthorizationTest.php b/tests/unit/Validator/AuthorizationTest.php index 37e5a6e22..08c1e46f2 100644 --- a/tests/unit/Validator/AuthorizationTest.php +++ b/tests/unit/Validator/AuthorizationTest.php @@ -9,15 +9,11 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Authorization\Input; class AuthorizationTest extends TestCase { - protected Authorization $authorization; - public function setUp(): void { - $this->authorization = new Authorization(); } public function tearDown(): void @@ -26,7 +22,7 @@ public function tearDown(): void public function testValues(): void { - $this->authorization->addRole(Role::any()->toString()); + Authorization::setRole(Role::any()->toString()); $document = new Document([ '$id' => ID::unique(), @@ -39,88 +35,88 @@ public function testValues(): void Permission::delete(Role::any()), ], ]); + $object = new Authorization(Database::PERMISSION_READ); - $object = $this->authorization; - - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, [])), false); + $this->assertEquals($object->isValid($document->getRead()), false); + $this->assertEquals($object->isValid(''), false); + $this->assertEquals($object->isValid([]), false); $this->assertEquals($object->getDescription(), 'No permissions provided for action \'read\''); - $this->authorization->addRole(Role::user('456')->toString()); - $this->authorization->addRole(Role::user('123')->toString()); + Authorization::setRole(Role::user('456')->toString()); + Authorization::setRole(Role::user('123')->toString()); - $this->assertEquals($this->authorization->isRole(Role::user('456')->toString()), true); - $this->assertEquals($this->authorization->isRole(Role::user('457')->toString()), false); - $this->assertEquals($this->authorization->isRole(''), false); - $this->assertEquals($this->authorization->isRole(Role::any()->toString()), true); + $this->assertEquals(Authorization::isRole(Role::user('456')->toString()), true); + $this->assertEquals(Authorization::isRole(Role::user('457')->toString()), false); + $this->assertEquals(Authorization::isRole(''), false); + $this->assertEquals(Authorization::isRole(Role::any()->toString()), true); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true); + $this->assertEquals($object->isValid($document->getRead()), true); - $this->authorization->cleanRoles(); + Authorization::cleanRoles(); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false); + $this->assertEquals($object->isValid($document->getRead()), false); - $this->authorization->addRole(Role::team('123')->toString()); + Authorization::setRole(Role::team('123')->toString()); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true); + $this->assertEquals($object->isValid($document->getRead()), true); - $this->authorization->cleanRoles(); - $this->authorization->disable(); + Authorization::cleanRoles(); + Authorization::disable(); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true); + $this->assertEquals($object->isValid($document->getRead()), true); - $this->authorization->reset(); + Authorization::reset(); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false); + $this->assertEquals($object->isValid($document->getRead()), false); - $this->authorization->setDefaultStatus(false); - $this->authorization->disable(); + Authorization::setDefaultStatus(false); + Authorization::disable(); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true); + $this->assertEquals($object->isValid($document->getRead()), true); - $this->authorization->reset(); + Authorization::reset(); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true); + $this->assertEquals($object->isValid($document->getRead()), true); - $this->authorization->enable(); + Authorization::enable(); - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false); + $this->assertEquals($object->isValid($document->getRead()), false); - $this->authorization->addRole('textX'); + Authorization::setRole('textX'); - $this->assertContains('textX', $this->authorization->getRoles()); + $this->assertContains('textX', Authorization::getRoles()); - $this->authorization->removeRole('textX'); + Authorization::unsetRole('textX'); - $this->assertNotContains('textX', $this->authorization->getRoles()); + $this->assertNotContains('textX', Authorization::getRoles()); // Test skip method - $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false); - $this->assertEquals($this->authorization->skip(function () use ($object, $document) { - return $object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())); + $this->assertEquals($object->isValid($document->getRead()), false); + $this->assertEquals(Authorization::skip(function () use ($object, $document) { + return $object->isValid($document->getRead()); }), true); } public function testNestedSkips(): void { - $this->assertEquals(true, $this->authorization->getStatus()); + $this->assertEquals(true, Authorization::$status); - $this->authorization->skip(function () { - $this->assertEquals(false, $this->authorization->getStatus()); + Authorization::skip(function () { + $this->assertEquals(false, Authorization::$status); - $this->authorization->skip(function () { - $this->assertEquals(false, $this->authorization->getStatus()); + Authorization::skip(function () { + $this->assertEquals(false, Authorization::$status); - $this->authorization->skip(function () { - $this->assertEquals(false, $this->authorization->getStatus()); + Authorization::skip(function () { + $this->assertEquals(false, Authorization::$status); }); - $this->assertEquals(false, $this->authorization->getStatus()); + $this->assertEquals(false, Authorization::$status); }); - $this->assertEquals(false, $this->authorization->getStatus()); + $this->assertEquals(false, Authorization::$status); }); - $this->assertEquals(true, $this->authorization->getStatus()); + $this->assertEquals(true, Authorization::$status); } } diff --git a/tests/unit/Validator/DateTimeTest.php b/tests/unit/Validator/DateTimeTest.php index d8ababb6f..b9157e8b8 100644 --- a/tests/unit/Validator/DateTimeTest.php +++ b/tests/unit/Validator/DateTimeTest.php @@ -128,7 +128,7 @@ public function testOffset(): void $threwException = false; try { $dateValidator = new DatetimeValidator(offset: -60); - } catch (\Exception $e) { + } catch(\Exception $e) { $threwException = true; } $this->assertTrue($threwException); From 169977b8ac2cc731ff86efaa56193e60a58ba02f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:07:12 +1300 Subject: [PATCH 251/256] Fix test refs --- tests/e2e/Adapter/Base.php | 344 ++++++++++++++++++------------------- 1 file changed, 172 insertions(+), 172 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index d9dd2ff77..9aba9ac99 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -65,7 +65,7 @@ public function testPing(): void public function testCreateExistsDelete(): void { - $schemaSupport = $this->getDatabase()->getAdapter()->getSupportForSchemas(); + $schemaSupport = static::getDatabase()->getAdapter()->getSupportForSchemas(); if (!$schemaSupport) { $this->assertEquals(static::getDatabase(), static::getDatabase()->setDatabase($this->testDatabase)); $this->assertEquals(true, static::getDatabase()->create()); @@ -751,7 +751,7 @@ public function testPreserveDatesUpdate(): void $doc1 = static::getDatabase()->getDocument('preserve_update_dates', 'doc1'); $this->assertEquals($newDate, $doc1->getAttribute('$updatedAt')); - $this->getDatabase()->updateDocuments( + static::getDatabase()->updateDocuments( 'preserve_update_dates', new Document([ '$updatedAt' => $newDate @@ -976,7 +976,7 @@ public function testCreatedAtUpdatedAt(): void public function testQueryTimeout(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForTimeouts()) { + if (!static::getDatabase()->getAdapter()->getSupportForTimeouts()) { $this->expectNotToPerformAssertions(); return; } @@ -1081,13 +1081,13 @@ public function testSizeCollection(): void static::getDatabase()->analyzeCollection('sizeTest2'); - $size2 = $this->getDatabase()->getSizeOfCollection('sizeTest2'); + $size2 = static::getDatabase()->getSizeOfCollection('sizeTest2'); $this->assertGreaterThan($size1, $size2); self::$authorization->skip(function () use ($loopCount) { for ($i = 0; $i < $loopCount; $i++) { - $this->getDatabase()->deleteDocument('sizeTest2', 'doc' . $i); + static::getDatabase()->deleteDocument('sizeTest2', 'doc' . $i); } }); @@ -1095,40 +1095,40 @@ public function testSizeCollection(): void static::getDatabase()->analyzeCollection('sizeTest2'); - $size3 = $this->getDatabase()->getSizeOfCollection('sizeTest2'); + $size3 = static::getDatabase()->getSizeOfCollection('sizeTest2'); $this->assertLessThan($size2, $size3); } public function testSizeCollectionOnDisk(): void { - $this->getDatabase()->createCollection('sizeTestDisk1'); - $this->getDatabase()->createCollection('sizeTestDisk2'); + static::getDatabase()->createCollection('sizeTestDisk1'); + static::getDatabase()->createCollection('sizeTestDisk2'); - $size1 = $this->getDatabase()->getSizeOfCollectionOnDisk('sizeTestDisk1'); - $size2 = $this->getDatabase()->getSizeOfCollectionOnDisk('sizeTestDisk2'); + $size1 = static::getDatabase()->getSizeOfCollectionOnDisk('sizeTestDisk1'); + $size2 = static::getDatabase()->getSizeOfCollectionOnDisk('sizeTestDisk2'); $sizeDifference = abs($size1 - $size2); // Size of an empty collection returns either 172032 or 167936 bytes randomly // Therefore asserting with a tolerance of 5000 bytes $byteDifference = 5000; $this->assertLessThan($byteDifference, $sizeDifference); - $this->getDatabase()->createAttribute('sizeTestDisk2', 'string1', Database::VAR_STRING, 20000, true); - $this->getDatabase()->createAttribute('sizeTestDisk2', 'string2', Database::VAR_STRING, 254 + 1, true); - $this->getDatabase()->createAttribute('sizeTestDisk2', 'string3', Database::VAR_STRING, 254 + 1, true); - $this->getDatabase()->createIndex('sizeTestDisk2', 'index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128]); + static::getDatabase()->createAttribute('sizeTestDisk2', 'string1', Database::VAR_STRING, 20000, true); + static::getDatabase()->createAttribute('sizeTestDisk2', 'string2', Database::VAR_STRING, 254 + 1, true); + static::getDatabase()->createAttribute('sizeTestDisk2', 'string3', Database::VAR_STRING, 254 + 1, true); + static::getDatabase()->createIndex('sizeTestDisk2', 'index', Database::INDEX_KEY, ['string1', 'string2', 'string3'], [128, 128, 128]); $loopCount = 40; for ($i = 0; $i < $loopCount; $i++) { - $this->getDatabase()->createDocument('sizeTestDisk2', new Document([ + static::getDatabase()->createDocument('sizeTestDisk2', new Document([ 'string1' => 'string1' . $i, 'string2' => 'string2' . $i, 'string3' => 'string3' . $i, ])); } - $size2 = $this->getDatabase()->getSizeOfCollectionOnDisk('sizeTestDisk2'); + $size2 = static::getDatabase()->getSizeOfCollectionOnDisk('sizeTestDisk2'); $this->assertGreaterThan($size1, $size2); } @@ -1143,7 +1143,7 @@ public function testSizeFullText(): void static::getDatabase()->createCollection('fullTextSizeTest'); - $size1 = $this->getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); + $size1 = static::getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); static::getDatabase()->createAttribute('fullTextSizeTest', 'string1', Database::VAR_STRING, 128, true); static::getDatabase()->createAttribute('fullTextSizeTest', 'string2', Database::VAR_STRING, 254, true); @@ -1160,13 +1160,13 @@ public function testSizeFullText(): void ])); } - $size2 = $this->getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); + $size2 = static::getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); $this->assertGreaterThan($size1, $size2); static::getDatabase()->createIndex('fullTextSizeTest', 'fulltext_index', Database::INDEX_FULLTEXT, ['string1']); - $size3 = $this->getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); + $size3 = static::getDatabase()->getSizeOfCollectionOnDisk('fullTextSizeTest'); $this->assertGreaterThan($size2, $size3); } @@ -2270,7 +2270,7 @@ public function testFulltextIndexWithInteger(): void { $this->expectException(Exception::class); - if (!$this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + if (!static::getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->expectExceptionMessage('Fulltext index is not supported'); } else { $this->expectExceptionMessage('Attribute "integer_signed" cannot be part of a FULLTEXT index, must be of type string'); @@ -2281,7 +2281,7 @@ public function testFulltextIndexWithInteger(): void public function testListDocumentSearch(): void { - $fulltextSupport = $this->getDatabase()->getAdapter()->getSupportForFulltextIndex(); + $fulltextSupport = static::getDatabase()->getAdapter()->getSupportForFulltextIndex(); if (!$fulltextSupport) { $this->expectNotToPerformAssertions(); return; @@ -2344,7 +2344,7 @@ public function testEmptyTenant(): void public function testEmptySearch(): void { - $fulltextSupport = $this->getDatabase()->getAdapter()->getSupportForFulltextIndex(); + $fulltextSupport = static::getDatabase()->getAdapter()->getSupportForFulltextIndex(); if (!$fulltextSupport) { $this->expectNotToPerformAssertions(); return; @@ -2381,7 +2381,7 @@ public function testUpdateDocument(Document $document): Document ->setAttribute('colors', 'red', Document::SET_TYPE_APPEND) ->setAttribute('with-dash', 'Works'); - $new = $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); + $new = static::getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); $this->assertNotEmpty(true, $new->getId()); $this->assertIsString($new->getAttribute('string')); @@ -2408,9 +2408,9 @@ public function testUpdateDocument(Document $document): Document ->setAttribute('$permissions', Permission::update(Role::guests()), Document::SET_TYPE_APPEND) ->setAttribute('$permissions', Permission::delete(Role::guests()), Document::SET_TYPE_APPEND); - $this->getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); + static::getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); - $new = $this->getDatabase()->getDocument($new->getCollection(), $new->getId()); + $new = static::getDatabase()->getDocument($new->getCollection(), $new->getId()); $this->assertContains('guests', $new->getRead()); $this->assertContains('guests', $new->getWrite()); @@ -2420,9 +2420,9 @@ public function testUpdateDocument(Document $document): Document $new->setAttribute('$permissions', $oldPermissions); - $this->getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); + static::getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); - $new = $this->getDatabase()->getDocument($new->getCollection(), $new->getId()); + $new = static::getDatabase()->getDocument($new->getCollection(), $new->getId()); $this->assertNotContains('guests', $new->getRead()); $this->assertNotContains('guests', $new->getWrite()); @@ -2434,12 +2434,12 @@ public function testUpdateDocument(Document $document): Document $id = $new->getId(); $newId = 'new-id'; $new->setAttribute('$id', $newId); - $new = $this->getDatabase()->updateDocument($new->getCollection(), $id, $new); + $new = static::getDatabase()->updateDocument($new->getCollection(), $id, $new); $this->assertEquals($newId, $new->getId()); // Reset ID $new->setAttribute('$id', $id); - $new = $this->getDatabase()->updateDocument($new->getCollection(), $newId, $new); + $new = static::getDatabase()->updateDocument($new->getCollection(), $newId, $new); $this->assertEquals($id, $new->getId()); return $document; @@ -2498,16 +2498,16 @@ public function testUpdateDocuments(array $documents): void public function testUpdateDocumentConflict(Document $document): void { $document->setAttribute('integer_signed', 7); - $result = $this->getDatabase()->withRequestTimestamp(new \DateTime(), function () use ($document) { - return $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); + $result = static::getDatabase()->withRequestTimestamp(new \DateTime(), function () use ($document) { + return static::getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); }); $this->assertEquals(7, $result->getAttribute('integer_signed')); $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); $document->setAttribute('integer_signed', 8); try { - $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($document) { - return $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); + static::getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($document) { + return static::getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); }); $this->fail('Failed to throw exception'); } catch(Throwable $e) { @@ -2523,8 +2523,8 @@ public function testDeleteDocumentConflict(Document $document): void { $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); $this->expectException(ConflictException::class); - $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($document) { - return $this->getDatabase()->deleteDocument($document->getCollection(), $document->getId()); + static::getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($document) { + return static::getDatabase()->deleteDocument($document->getCollection(), $document->getId()); }); } @@ -2533,7 +2533,7 @@ public function testDeleteDocumentConflict(Document $document): void */ public function testUpdateDocumentDuplicatePermissions(Document $document): Document { - $new = $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); + $new = static::getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); $new ->setAttribute('$permissions', Permission::read(Role::guests()), Document::SET_TYPE_APPEND) @@ -2541,9 +2541,9 @@ public function testUpdateDocumentDuplicatePermissions(Document $document): Docu ->setAttribute('$permissions', Permission::create(Role::guests()), Document::SET_TYPE_APPEND) ->setAttribute('$permissions', Permission::create(Role::guests()), Document::SET_TYPE_APPEND); - $this->getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); + static::getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); - $new = $this->getDatabase()->getDocument($new->getCollection(), $new->getId()); + $new = static::getDatabase()->getDocument($new->getCollection(), $new->getId()); $this->assertContains('guests', $new->getRead()); $this->assertContains('guests', $new->getCreate()); @@ -2556,8 +2556,8 @@ public function testUpdateDocumentDuplicatePermissions(Document $document): Docu */ public function testDeleteDocument(Document $document): void { - $result = $this->getDatabase()->deleteDocument($document->getCollection(), $document->getId()); - $document = $this->getDatabase()->getDocument($document->getCollection(), $document->getId()); + $result = static::getDatabase()->deleteDocument($document->getCollection(), $document->getId()); + $document = static::getDatabase()->getDocument($document->getCollection(), $document->getId()); $this->assertEquals(true, $result); $this->assertEquals(true, $document->isEmpty()); @@ -2728,7 +2728,7 @@ public function testArrayAttribute(): void $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); } catch(Throwable $e) { - if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + if (static::getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); } else { $this->assertEquals('Fulltext index is not supported', $e->getMessage()); @@ -2777,7 +2777,7 @@ public function testArrayAttribute(): void $this->assertTrue($database->createIndex($collection, 'indx6', Database::INDEX_KEY, ['age', 'names'], [null, 999], [])); $this->assertTrue($database->createIndex($collection, 'indx7', Database::INDEX_KEY, ['age', 'booleans'], [0, 999], [])); - if ($this->getDatabase()->getAdapter()->getSupportForQueryContains()) { + if (static::getDatabase()->getAdapter()->getSupportForQueryContains()) { try { $database->find($collection, [ Query::equal('names', ['Joe']), @@ -3197,7 +3197,7 @@ public function testFindFloat(): void public function testFindContains(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForQueryContains()) { + if (!static::getDatabase()->getAdapter()->getSupportForQueryContains()) { $this->expectNotToPerformAssertions(); return; } @@ -3239,7 +3239,7 @@ public function testFindFulltext(): void /** * Fulltext search */ - if ($this->getDatabase()->getAdapter()->getSupportForFulltextIndex()) { + if (static::getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $success = static::getDatabase()->createIndex('movies', 'name', Database::INDEX_FULLTEXT, ['name']); $this->assertEquals(true, $success); @@ -3256,7 +3256,7 @@ public function testFindFulltext(): void // TODO: Looks like the MongoDB implementation is a bit more complex, skipping that for now. // TODO: I think this needs a changes? how do we distinguish between regular full text and wildcard? - if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) { + if (static::getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) { $documents = static::getDatabase()->find('movies', [ Query::search('name', 'cap'), ]); @@ -4337,7 +4337,7 @@ public function testFindStartsWith(): void $this->assertEquals(2, count($documents)); - if ($this->getDatabase()->getAdapter() instanceof SQL) { + if (static::getDatabase()->getAdapter() instanceof SQL) { $documents = static::getDatabase()->find('movies', [ Query::startsWith('name', '%ork'), ]); @@ -5145,10 +5145,10 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void public function testExceptionAttributeLimit(): void { - if ($this->getDatabase()->getLimitForAttributes() > 0) { + if (static::getDatabase()->getLimitForAttributes() > 0) { // Load the collection up to the limit $attributes = []; - for ($i = 0; $i < $this->getDatabase()->getLimitForAttributes(); $i++) { + for ($i = 0; $i < static::getDatabase()->getLimitForAttributes(); $i++) { $attributes[] = new Document([ '$id' => ID::custom("test{$i}"), 'type' => Database::VAR_INTEGER, @@ -5176,7 +5176,7 @@ public function testExceptionAttributeLimit(): void */ public function testCheckAttributeCountLimit(): void { - if ($this->getDatabase()->getLimitForAttributes() > 0) { + if (static::getDatabase()->getLimitForAttributes() > 0) { $collection = static::getDatabase()->getCollection('attributeLimit'); // create same attribute in testExceptionAttributeLimit @@ -5338,7 +5338,7 @@ public function testExceptionIndexLimit(): void // Testing for indexLimit // Add up to the limit, then check if the next index throws IndexLimitException - for ($i = 0; $i < ($this->getDatabase()->getLimitForIndexes()); $i++) { + for ($i = 0; $i < (static::getDatabase()->getLimitForIndexes()); $i++) { $this->assertEquals(true, static::getDatabase()->createIndex('indexLimit', "index{$i}", Database::INDEX_KEY, ["test{$i}"], [16])); } $this->expectException(LimitException::class); @@ -5447,12 +5447,12 @@ public function testUniqueIndexDuplicateUpdate(): void public function testGetAttributeLimit(): void { - $this->assertIsInt($this->getDatabase()->getLimitForAttributes()); + $this->assertIsInt(static::getDatabase()->getLimitForAttributes()); } public function testGetIndexLimit(): void { - $this->assertEquals(58, $this->getDatabase()->getLimitForIndexes()); + $this->assertEquals(58, static::getDatabase()->getLimitForIndexes()); } public function testGetId(): void @@ -15690,7 +15690,7 @@ public function testDeleteBulkDocuments(): void $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); $this->assertEquals(5, $deleted); - $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete'))); + $this->assertEquals(0, count(static::getDatabase()->find('bulk_delete'))); // TEST: Make sure we can't delete documents we don't have permissions for static::getDatabase()->updateCollection('bulk_delete', [ @@ -15713,7 +15713,7 @@ public function testDeleteBulkDocuments(): void Permission::delete(Role::any()) ], false); static::getDatabase()->deleteDocuments('bulk_delete'); - $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete'))); + $this->assertEquals(0, count(static::getDatabase()->find('bulk_delete'))); // Teardown static::getDatabase()->deleteCollection('bulk_delete'); @@ -15726,22 +15726,22 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void return; } - $this->getDatabase()->createCollection('bulk_delete_person_o2o'); - $this->getDatabase()->createCollection('bulk_delete_library_o2o'); + static::getDatabase()->createCollection('bulk_delete_person_o2o'); + static::getDatabase()->createCollection('bulk_delete_library_o2o'); - $this->getDatabase()->createAttribute('bulk_delete_person_o2o', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_o2o', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_o2o', 'area', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_person_o2o', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_o2o', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_o2o', 'area', Database::VAR_STRING, 255, true); // Restrict - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'bulk_delete_person_o2o', relatedCollection: 'bulk_delete_library_o2o', type: Database::RELATION_ONE_TO_ONE, onDelete: Database::RELATION_MUTATE_RESTRICT ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -15761,20 +15761,20 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); $library = $person1->getAttribute('bulk_delete_library_o2o'); $this->assertEquals('library1', $library['$id']); $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); // Delete person try { - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2o'); $this->fail('Failed to throw exception'); } catch (RestrictedException $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } - $this->getDatabase()->updateDocument('bulk_delete_person_o2o', 'person1', new Document([ + static::getDatabase()->updateDocument('bulk_delete_person_o2o', 'person1', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -15785,19 +15785,19 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void 'bulk_delete_library_o2o' => null, ])); - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2o')); // NULL - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'bulk_delete_person_o2o', id: 'bulk_delete_library_o2o', onDelete: Database::RELATION_MUTATE_SET_NULL ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -15817,35 +15817,35 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); $library = $person1->getAttribute('bulk_delete_library_o2o'); $this->assertEquals('library1', $library['$id']); $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); - $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); - $this->assertCount(1, $this->getDatabase()->find('bulk_delete_person_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2o')); + $this->assertCount(1, static::getDatabase()->find('bulk_delete_person_o2o')); - $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); $library = $person->getAttribute('bulk_delete_library_o2o'); $this->assertNull($library); // NULL - Cleanup - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2o')); // Cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'bulk_delete_person_o2o', id: 'bulk_delete_library_o2o', onDelete: Database::RELATION_MUTATE_CASCADE ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -15865,27 +15865,27 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); $library = $person1->getAttribute('bulk_delete_library_o2o'); $this->assertEquals('library1', $library['$id']); $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); - $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); - $this->assertCount(1, $this->getDatabase()->find('bulk_delete_person_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2o')); + $this->assertCount(1, static::getDatabase()->find('bulk_delete_person_o2o')); - $person = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); $library = $person->getAttribute('bulk_delete_library_o2o'); $this->assertEmpty($library); $this->assertNotNull($library); // Test Bulk delete parent - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_o2o')); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2o', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -15905,14 +15905,14 @@ public function testDeleteBulkDocumentsOneToOneRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2o', 'person1'); $library = $person1->getAttribute('bulk_delete_library_o2o'); $this->assertEquals('library1', $library['$id']); $this->assertArrayNotHasKey('bulk_delete_person_o2o', $library); - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2o')); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2o')); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_o2o')); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2o')); } public function testDeleteBulkDocumentsOneToManyRelationship(): void @@ -15922,22 +15922,22 @@ public function testDeleteBulkDocumentsOneToManyRelationship(): void return; } - $this->getDatabase()->createCollection('bulk_delete_person_o2m'); - $this->getDatabase()->createCollection('bulk_delete_library_o2m'); + static::getDatabase()->createCollection('bulk_delete_person_o2m'); + static::getDatabase()->createCollection('bulk_delete_library_o2m'); - $this->getDatabase()->createAttribute('bulk_delete_person_o2m', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_o2m', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_o2m', 'area', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_person_o2m', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_o2m', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_o2m', 'area', Database::VAR_STRING, 255, true); // Restrict - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'bulk_delete_person_o2m', relatedCollection: 'bulk_delete_library_o2m', type: Database::RELATION_ONE_TO_MANY, onDelete: Database::RELATION_MUTATE_RESTRICT ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -15969,33 +15969,33 @@ public function testDeleteBulkDocumentsOneToManyRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); $libraries = $person1->getAttribute('bulk_delete_library_o2m'); $this->assertCount(2, $libraries); // Delete person try { - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2m'); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2m'); $this->fail('Failed to throw exception'); } catch (RestrictedException $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Restrict Cleanup - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2m')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2m')); - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2m')); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_o2m')); // NULL - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'bulk_delete_person_o2m', id: 'bulk_delete_library_o2m', onDelete: Database::RELATION_MUTATE_SET_NULL ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -16027,30 +16027,30 @@ public function testDeleteBulkDocumentsOneToManyRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); $libraries = $person1->getAttribute('bulk_delete_library_o2m'); $this->assertCount(2, $libraries); - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2m')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2m')); - $person = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $person = static::getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); $libraries = $person->getAttribute('bulk_delete_library_o2m'); $this->assertEmpty($libraries); // NULL - Cleanup - $this->getDatabase()->deleteDocuments('bulk_delete_person_o2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_o2m')); + static::getDatabase()->deleteDocuments('bulk_delete_person_o2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_o2m')); // Cascade - $this->getDatabase()->updateRelationship( + static::getDatabase()->updateRelationship( collection: 'bulk_delete_person_o2m', id: 'bulk_delete_library_o2m', onDelete: Database::RELATION_MUTATE_CASCADE ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_o2m', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -16082,14 +16082,14 @@ public function testDeleteBulkDocumentsOneToManyRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); $libraries = $person1->getAttribute('bulk_delete_library_o2m'); $this->assertCount(2, $libraries); - $this->getDatabase()->deleteDocuments('bulk_delete_library_o2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_o2m')); + static::getDatabase()->deleteDocuments('bulk_delete_library_o2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_o2m')); - $person = $this->getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); + $person = static::getDatabase()->getDocument('bulk_delete_person_o2m', 'person1'); $libraries = $person->getAttribute('bulk_delete_library_o2m'); $this->assertEmpty($libraries); } @@ -16101,22 +16101,22 @@ public function testDeleteBulkDocumentsManyToManyRelationship(): void return; } - $this->getDatabase()->createCollection('bulk_delete_person_m2m'); - $this->getDatabase()->createCollection('bulk_delete_library_m2m'); + static::getDatabase()->createCollection('bulk_delete_person_m2m'); + static::getDatabase()->createCollection('bulk_delete_library_m2m'); - $this->getDatabase()->createAttribute('bulk_delete_person_m2m', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_m2m', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_m2m', 'area', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_person_m2m', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_m2m', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_m2m', 'area', Database::VAR_STRING, 255, true); // Many-to-Many Relationship - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'bulk_delete_person_m2m', relatedCollection: 'bulk_delete_library_m2m', type: Database::RELATION_MANY_TO_MANY, onDelete: Database::RELATION_MUTATE_RESTRICT ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_m2m', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_m2m', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -16148,25 +16148,25 @@ public function testDeleteBulkDocumentsManyToManyRelationship(): void ], ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_m2m', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_m2m', 'person1'); $libraries = $person1->getAttribute('bulk_delete_library_m2m'); $this->assertCount(2, $libraries); // Delete person try { - $this->getDatabase()->deleteDocuments('bulk_delete_person_m2m'); + static::getDatabase()->deleteDocuments('bulk_delete_person_m2m'); $this->fail('Failed to throw exception'); } catch (RestrictedException $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } // Restrict Cleanup - $this->getDatabase()->deleteRelationship('bulk_delete_person_m2m', 'bulk_delete_library_m2m'); - $this->getDatabase()->deleteDocuments('bulk_delete_library_m2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_m2m')); + static::getDatabase()->deleteRelationship('bulk_delete_person_m2m', 'bulk_delete_library_m2m'); + static::getDatabase()->deleteDocuments('bulk_delete_library_m2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_m2m')); - $this->getDatabase()->deleteDocuments('bulk_delete_person_m2m'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_m2m')); + static::getDatabase()->deleteDocuments('bulk_delete_person_m2m'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_m2m')); } public function testDeleteBulkDocumentsManyToOneRelationship(): void @@ -16176,22 +16176,22 @@ public function testDeleteBulkDocumentsManyToOneRelationship(): void return; } - $this->getDatabase()->createCollection('bulk_delete_person_m2o'); - $this->getDatabase()->createCollection('bulk_delete_library_m2o'); + static::getDatabase()->createCollection('bulk_delete_person_m2o'); + static::getDatabase()->createCollection('bulk_delete_library_m2o'); - $this->getDatabase()->createAttribute('bulk_delete_person_m2o', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_m2o', 'name', Database::VAR_STRING, 255, true); - $this->getDatabase()->createAttribute('bulk_delete_library_m2o', 'area', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_person_m2o', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_m2o', 'name', Database::VAR_STRING, 255, true); + static::getDatabase()->createAttribute('bulk_delete_library_m2o', 'area', Database::VAR_STRING, 255, true); // Many-to-One Relationship - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'bulk_delete_person_m2o', relatedCollection: 'bulk_delete_library_m2o', type: Database::RELATION_MANY_TO_ONE, onDelete: Database::RELATION_MUTATE_RESTRICT ); - $person1 = $this->getDatabase()->createDocument('bulk_delete_person_m2o', new Document([ + $person1 = static::getDatabase()->createDocument('bulk_delete_person_m2o', new Document([ '$id' => 'person1', '$permissions' => [ Permission::read(Role::any()), @@ -16211,7 +16211,7 @@ public function testDeleteBulkDocumentsManyToOneRelationship(): void ], ])); - $person2 = $this->getDatabase()->createDocument('bulk_delete_person_m2o', new Document([ + $person2 = static::getDatabase()->createDocument('bulk_delete_person_m2o', new Document([ '$id' => 'person2', '$permissions' => [ Permission::read(Role::any()), @@ -16224,30 +16224,30 @@ public function testDeleteBulkDocumentsManyToOneRelationship(): void ] ])); - $person1 = $this->getDatabase()->getDocument('bulk_delete_person_m2o', 'person1'); + $person1 = static::getDatabase()->getDocument('bulk_delete_person_m2o', 'person1'); $library = $person1->getAttribute('bulk_delete_library_m2o'); $this->assertEquals('library1', $library['$id']); // Delete library try { - $this->getDatabase()->deleteDocuments('bulk_delete_library_m2o'); + static::getDatabase()->deleteDocuments('bulk_delete_library_m2o'); $this->fail('Failed to throw exception'); } catch (RestrictedException $e) { $this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage()); } - $this->assertEquals(2, count($this->getDatabase()->find('bulk_delete_person_m2o'))); + $this->assertEquals(2, count(static::getDatabase()->find('bulk_delete_person_m2o'))); // Test delete people - $this->getDatabase()->deleteDocuments('bulk_delete_person_m2o'); - $this->assertEquals(0, count($this->getDatabase()->find('bulk_delete_person_m2o'))); + static::getDatabase()->deleteDocuments('bulk_delete_person_m2o'); + $this->assertEquals(0, count(static::getDatabase()->find('bulk_delete_person_m2o'))); // Restrict Cleanup - $this->getDatabase()->deleteDocuments('bulk_delete_library_m2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_m2o')); + static::getDatabase()->deleteDocuments('bulk_delete_library_m2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_library_m2o')); - $this->getDatabase()->deleteDocuments('bulk_delete_person_m2o'); - $this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_m2o')); + static::getDatabase()->deleteDocuments('bulk_delete_person_m2o'); + $this->assertCount(0, static::getDatabase()->find('bulk_delete_person_m2o')); } public function testUpdateDocuments(): void @@ -16523,7 +16523,7 @@ public function testUpdateDocumentsPermissions(): void $documents = static::$authorization ->skip(function () use ($collection) { - return $this->getDatabase()->find($collection); + return static::getDatabase()->find($collection); }); $this->assertCount(11, $documents); @@ -16544,7 +16544,7 @@ public function testUpdateDocumentsPermissions(): void public function testUpdateDocumentsRelationships(): void { - if (!$this->getDatabase()->getAdapter()->getSupportForBatchOperations() || !$this->getDatabase()->getAdapter()->getSupportForRelationships()) { + if (!static::getDatabase()->getAdapter()->getSupportForBatchOperations() || !static::getDatabase()->getAdapter()->getSupportForRelationships()) { $this->expectNotToPerformAssertions(); return; } @@ -16552,7 +16552,7 @@ public function testUpdateDocumentsRelationships(): void self::$authorization->cleanRoles(); self::$authorization->addRole(Role::any()->toString()); - $this->getDatabase()->createCollection('testUpdateDocumentsRelationships1', attributes: [ + static::getDatabase()->createCollection('testUpdateDocumentsRelationships1', attributes: [ new Document([ '$id' => ID::custom('string'), 'type' => Database::VAR_STRING, @@ -16566,7 +16566,7 @@ public function testUpdateDocumentsRelationships(): void Permission::delete(Role::any()) ]); - $this->getDatabase()->createCollection('testUpdateDocumentsRelationships2', attributes: [ + static::getDatabase()->createCollection('testUpdateDocumentsRelationships2', attributes: [ new Document([ '$id' => ID::custom('string'), 'type' => Database::VAR_STRING, @@ -16580,46 +16580,46 @@ public function testUpdateDocumentsRelationships(): void Permission::delete(Role::any()) ]); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'testUpdateDocumentsRelationships1', relatedCollection: 'testUpdateDocumentsRelationships2', type: Database::RELATION_ONE_TO_ONE, twoWay: true, ); - $this->getDatabase()->createDocument('testUpdateDocumentsRelationships1', new Document([ + static::getDatabase()->createDocument('testUpdateDocumentsRelationships1', new Document([ '$id' => 'doc1', 'string' => 'text📝', ])); - $this->getDatabase()->createDocument('testUpdateDocumentsRelationships2', new Document([ + static::getDatabase()->createDocument('testUpdateDocumentsRelationships2', new Document([ '$id' => 'doc1', 'string' => 'text📝', 'testUpdateDocumentsRelationships1' => 'doc1' ])); - $sisterDocument = $this->getDatabase()->getDocument('testUpdateDocumentsRelationships2', 'doc1'); + $sisterDocument = static::getDatabase()->getDocument('testUpdateDocumentsRelationships2', 'doc1'); $this->assertNotNull($sisterDocument); - $this->getDatabase()->updateDocuments('testUpdateDocumentsRelationships1', new Document([ + static::getDatabase()->updateDocuments('testUpdateDocumentsRelationships1', new Document([ 'string' => 'text📝 updated', ])); - $document = $this->getDatabase()->findOne('testUpdateDocumentsRelationships1'); + $document = static::getDatabase()->findOne('testUpdateDocumentsRelationships1'); $this->assertNotFalse($document); $this->assertEquals('text📝 updated', $document->getAttribute('string')); - $sisterDocument = $this->getDatabase()->getDocument('testUpdateDocumentsRelationships2', 'doc1'); + $sisterDocument = static::getDatabase()->getDocument('testUpdateDocumentsRelationships2', 'doc1'); $this->assertNotNull($sisterDocument); $relationalDocument = $sisterDocument->getAttribute('testUpdateDocumentsRelationships1'); $this->assertEquals('text📝 updated', $relationalDocument->getAttribute('string')); // Check relationship value updating between each other. - $this->getDatabase()->deleteRelationship('testUpdateDocumentsRelationships1', 'testUpdateDocumentsRelationships2'); + static::getDatabase()->deleteRelationship('testUpdateDocumentsRelationships1', 'testUpdateDocumentsRelationships2'); - $this->getDatabase()->createRelationship( + static::getDatabase()->createRelationship( collection: 'testUpdateDocumentsRelationships1', relatedCollection: 'testUpdateDocumentsRelationships2', type: Database::RELATION_ONE_TO_MANY, @@ -16627,27 +16627,27 @@ public function testUpdateDocumentsRelationships(): void ); for ($i = 2; $i < 11; $i++) { - $this->getDatabase()->createDocument('testUpdateDocumentsRelationships1', new Document([ + static::getDatabase()->createDocument('testUpdateDocumentsRelationships1', new Document([ '$id' => 'doc' . $i, 'string' => 'text📝', ])); - $this->getDatabase()->createDocument('testUpdateDocumentsRelationships2', new Document([ + static::getDatabase()->createDocument('testUpdateDocumentsRelationships2', new Document([ '$id' => 'doc' . $i, 'string' => 'text📝', 'testUpdateDocumentsRelationships1' => 'doc' . $i ])); } - $this->getDatabase()->updateDocuments('testUpdateDocumentsRelationships2', new Document([ + static::getDatabase()->updateDocuments('testUpdateDocumentsRelationships2', new Document([ 'testUpdateDocumentsRelationships1' => null ])); - $this->getDatabase()->updateDocuments('testUpdateDocumentsRelationships2', new Document([ + static::getDatabase()->updateDocuments('testUpdateDocumentsRelationships2', new Document([ 'testUpdateDocumentsRelationships1' => 'doc1' ])); - $documents = $this->getDatabase()->find('testUpdateDocumentsRelationships2'); + $documents = static::getDatabase()->find('testUpdateDocumentsRelationships2'); foreach ($documents as $document) { $this->assertEquals('doc1', $document->getAttribute('testUpdateDocumentsRelationships1')->getId()); @@ -16690,7 +16690,7 @@ public function testEvents(): void $this->assertEquals($shifted, $event); }); - if ($this->getDatabase()->getAdapter()->getSupportForSchemas()) { + if (static::getDatabase()->getAdapter()->getSupportForSchemas()) { $database->setDatabase('hellodb'); $database->create(); } else { From 0cbdb248443be23dfe588b89f6bf01df530cc677 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:07:18 +1300 Subject: [PATCH 252/256] Update cache --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a99e03468..d24cea9a1 100755 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "ext-mbstring": "*", "php": ">=8.0", "utopia-php/framework": "0.33.*", - "utopia-php/cache": "0.10.*", + "utopia-php/cache": "0.11.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { From 1c0edebab050443921f1e0629a4f5e64840a2062 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:13:04 +1300 Subject: [PATCH 253/256] Update framework --- tests/e2e/Adapter/Base.php | 85 +++++++++----------------------------- 1 file changed, 19 insertions(+), 66 deletions(-) diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 9aba9ac99..1ac815705 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1085,7 +1085,7 @@ public function testSizeCollection(): void $this->assertGreaterThan($size1, $size2); - self::$authorization->skip(function () use ($loopCount) { + Authorization::skip(function () use ($loopCount) { for ($i = 0; $i < $loopCount; $i++) { static::getDatabase()->deleteDocument('sizeTest2', 'doc' . $i); } @@ -2445,58 +2445,12 @@ public function testUpdateDocument(Document $document): Document return $document; } - /** - * @depends testCreateDocuments - * @param array $documents - */ - public function testUpdateDocuments(array $documents): void - { - $collection = 'testCreateDocuments'; - - foreach ($documents as $document) { - $document - ->setAttribute('string', 'text📝 updated') - ->setAttribute('integer', 6) - ->setAttribute('$permissions', [ - Permission::read(Role::users()), - Permission::create(Role::users()), - Permission::update(Role::users()), - Permission::delete(Role::users()), - ]); - } - - $documents = static::getDatabase()->updateDocuments( - $collection, - $documents, - \count($documents) - ); - - foreach ($documents as $document) { - $this->assertEquals('text📝 updated', $document->getAttribute('string')); - $this->assertEquals(6, $document->getAttribute('integer')); - } - - $documents = static::getDatabase()->find($collection, [ - Query::limit(\count($documents)) - ]); - - foreach ($documents as $document) { - $this->assertEquals('text📝 updated', $document->getAttribute('string')); - $this->assertEquals(6, $document->getAttribute('integer')); - $this->assertEquals([ - Permission::read(Role::users()), - Permission::create(Role::users()), - Permission::update(Role::users()), - Permission::delete(Role::users()), - ], $document->getAttribute('$permissions')); - } - } - /** * @depends testUpdateDocument */ public function testUpdateDocumentConflict(Document $document): void { + $document->setAttribute('integer_signed', 7); $result = static::getDatabase()->withRequestTimestamp(new \DateTime(), function () use ($document) { return static::getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); @@ -15701,7 +15655,7 @@ public function testDeleteBulkDocuments(): void $deleted = static::getDatabase()->deleteDocuments('bulk_delete'); $this->assertEquals(0, $deleted); - $documents = static::$authorization->skip(function () { + $documents = Authorization::skip(function () { return static::getDatabase()->find('bulk_delete'); }); @@ -16258,8 +16212,8 @@ public function testUpdateDocuments(): void } $collection = 'testUpdateDocuments'; - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); static::getDatabase()->createCollection($collection, attributes: [ new Document([ @@ -16363,7 +16317,7 @@ public function testUpdateDocuments(): void // Check document level permissions static::getDatabase()->updateCollection($collection, permissions: [], documentSecurity: true); - static::$authorization->skip(function () use ($collection) { + Authorization::skip(function () use ($collection) { static::getDatabase()->updateDocument($collection, 'doc0', new Document([ 'string' => 'text📝 updated all', '$permissions' => [ @@ -16375,7 +16329,7 @@ public function testUpdateDocuments(): void ])); }); - static::$authorization->addRole(Role::user('asd')->toString()); + Authorization::setRole(Role::user('asd')->toString()); static::getDatabase()->updateDocuments($collection, new Document([ 'string' => 'permission text', @@ -16387,7 +16341,7 @@ public function testUpdateDocuments(): void $this->assertCount(1, $documents); - self::$authorization->skip(function () use ($collection) { + Authorization::skip(function () use ($collection) { $unmodifiedDocuments = static::getDatabase()->find($collection, [ Query::equal('string', ['text📝 updated all']), ]); @@ -16395,7 +16349,7 @@ public function testUpdateDocuments(): void $this->assertCount(9, $unmodifiedDocuments); }); - static::$authorization->skip(function () use ($collection) { + Authorization::skip(function () use ($collection) { static::getDatabase()->updateDocuments($collection, new Document([ '$permissions' => [ Permission::read(Role::any()), @@ -16419,8 +16373,8 @@ public function testUpdateDocuments(): void $this->assertEquals('batchSize Test', $document->getAttribute('string')); } - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); } public function testUpdateDocumentsPermissions(): void @@ -16442,7 +16396,7 @@ public function testUpdateDocumentsPermissions(): void ], permissions: [], documentSecurity: true); // Test we can bulk update permissions we have access to - static::$authorization->skip(function () use ($collection) { + Authorization::skip(function () use ($collection) { for ($i = 0; $i < 10; $i++) { static::getDatabase()->createDocument($collection, new Document([ '$id' => 'doc' . $i, @@ -16477,7 +16431,7 @@ public function testUpdateDocumentsPermissions(): void ], ])); - $documents = static::$authorization->skip(function () use ($collection) { + $documents = Authorization::skip(function () use ($collection) { return static::getDatabase()->find($collection); }); @@ -16506,7 +16460,7 @@ public function testUpdateDocumentsPermissions(): void $this->assertCount(1, $unmodifiedDocuments); - static::$authorization->addRole(Role::user('user2')->toString()); + Authorization::setRole(Role::user('user2')->toString()); // Test Bulk permission update with data $affected = static::getDatabase()->updateDocuments($collection, new Document([ @@ -16521,10 +16475,9 @@ public function testUpdateDocumentsPermissions(): void $this->assertEquals(10, $affected); - $documents = static::$authorization - ->skip(function () use ($collection) { - return static::getDatabase()->find($collection); - }); + $documents = Authorization::skip(function () use ($collection) { + return static::getDatabase()->find($collection); + }); $this->assertCount(11, $documents); @@ -16549,8 +16502,8 @@ public function testUpdateDocumentsRelationships(): void return; } - self::$authorization->cleanRoles(); - self::$authorization->addRole(Role::any()->toString()); + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); static::getDatabase()->createCollection('testUpdateDocumentsRelationships1', attributes: [ new Document([ From 15c0b000a8a9221cdd956d145cae1cd7acb48441 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:13:17 +1300 Subject: [PATCH 254/256] Update test auth refs --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 2021f0b9e..455293f53 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8a6537ec5c4f47cd0b6c34e9ceda042b", + "content-hash": "0ad14b2121f07c2fe936eaaa82186cbc", "packages": [ { "name": "brick/math", @@ -2002,16 +2002,16 @@ }, { "name": "utopia-php/cache", - "version": "0.10.2", + "version": "0.11.0", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" + "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", - "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/8ebcab5aac7606331cef69b0081f6c9eff2e58bc", + "reference": "8ebcab5aac7606331cef69b0081f6c9eff2e58bc", "shasum": "" }, "require": { @@ -2022,7 +2022,7 @@ }, "require-dev": { "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", + "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.13.1" }, @@ -2046,9 +2046,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.10.2" + "source": "https://github.com/utopia-php/cache/tree/0.11.0" }, - "time": "2024-06-25T20:36:35+00:00" + "time": "2024-11-05T16:53:58+00:00" }, { "name": "utopia-php/compression", From 7c4237e7e3b1c12238dd28d41e3478d2f0739326 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:53:47 +1300 Subject: [PATCH 255/256] Format --- composer.json | 12 +++++-- src/Database/Adapter/MariaDB.php | 18 +++++------ src/Database/Adapter/Mongo.php | 7 ++--- src/Database/Adapter/Postgres.php | 10 +++--- src/Database/Adapter/SQL.php | 6 ++-- src/Database/Adapter/SQLite.php | 8 ++--- src/Database/Exception.php | 2 +- src/Database/Query.php | 4 +-- src/Database/Validator/Datetime.php | 18 +++++------ src/Database/Validator/Index.php | 14 ++++----- src/Database/Validator/Queries.php | 4 +-- src/Database/Validator/Query/Filter.php | 18 +++++------ src/Database/Validator/Structure.php | 2 +- tests/e2e/Adapter/Base.php | 42 ++++++++++++------------- tests/unit/Validator/DateTimeTest.php | 2 +- 15 files changed, 86 insertions(+), 81 deletions(-) diff --git a/composer.json b/composer.json index d24cea9a1..2f99ab9f2 100755 --- a/composer.json +++ b/composer.json @@ -27,15 +27,15 @@ "Composer\\Config::disableProcessTimeout", "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml" ], - "lint": "./vendor/bin/pint --test", - "format": "./vendor/bin/pint", + "lint": "php -d memory_limit=2G ./vendor/bin/pint --test", + "format": "php -d memory_limit=2G ./vendor/bin/pint", "check": "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G", "coverage": "./vendor/bin/coverage-check ./tmp/clover.xml 90" }, "require": { + "php": ">=8.3", "ext-pdo": "*", "ext-mbstring": "*", - "php": ">=8.0", "utopia-php/framework": "0.33.*", "utopia-php/cache": "0.11.*", "utopia-php/mongo": "0.3.*" @@ -55,5 +55,11 @@ "ext-redis": "Needed to support Redis Cache Adapter", "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", "mongodb/mongodb": "Needed to support MongoDB Database Adapter" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "tbachert/spi": true + } } } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 73bf4d6a4..2aa524c52 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -731,7 +731,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes[$i] = "`{$attr}`{$length} {$order}"; - if(!empty($collectionAttribute['array']) && $this->castIndexArray()) { + if (!empty($collectionAttribute['array']) && $this->castIndexArray()) { $attributes[$i] = '(CAST(' . $attr . ' AS char(' . Database::ARRAY_INDEX_LENGTH . ') ARRAY))'; } } @@ -915,7 +915,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions->execute(); } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -963,7 +963,7 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); - if(!empty($document->getInternalId())) { + if (!empty($document->getInternalId())) { $attributes['_id'] = $document->getInternalId(); } @@ -1043,7 +1043,7 @@ public function createDocuments(string $collection, array $documents, int $batch } } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1291,7 +1291,7 @@ public function updateDocument(string $collection, string $id, Document $documen } } catch (\Throwable $e) { - if($e instanceof PDOException) { + if ($e instanceof PDOException) { switch ($e->getCode()) { case 1062: case 23000: @@ -1848,7 +1848,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -1976,7 +1976,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $queries = array_map(fn ($query) => clone $query, $queries); $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -2193,7 +2193,7 @@ protected function getSQLCondition(Query $query): string return "`table_main`.{$attribute} {$this->getSQLOperator($query->getMethod())}"; case Query::TYPE_CONTAINS: - if($this->getSupportForJSONOverlaps() && $query->onArray()) { + if ($this->getSupportForJSONOverlaps() && $query->onArray()) { return "JSON_OVERLAPS(`table_main`.{$attribute}, :{$placeholder}_0)"; } @@ -2219,7 +2219,7 @@ protected function getSQLCondition(Query $query): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if($array === true) { + if ($array === true) { return 'JSON'; } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 1bd7a095b..4b4215a36 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -3,7 +3,6 @@ namespace Utopia\Database\Adapter; use Exception; - use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; @@ -302,7 +301,7 @@ public function getSizeOfCollection(string $collection): int } else { throw new DatabaseException('No size found'); } - } catch(Exception $e) { + } catch (Exception $e) { throw new DatabaseException('Failed to get collection size: ' . $e->getMessage()); } } @@ -1434,7 +1433,7 @@ protected function buildFilters(array $queries, string $separator = '$and'): arr $queries = Query::groupByType($queries)['filters']; foreach ($queries as $query) { /* @var $query Query */ - if($query->isNested()) { + if ($query->isNested()) { $operator = $this->getQueryOperator($query->getMethod()); $filters[$separator][] = $this->buildFilters($query->getValues(), $operator); } else { @@ -1488,7 +1487,7 @@ protected function buildFilter(Query $query): array } elseif ($operator == '$ne' && \is_array($value)) { $filter[$attribute]['$nin'] = $value; } elseif ($operator == '$in') { - if($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { + if ($query->getMethod() === Query::TYPE_CONTAINS && !$query->onArray()) { $filter[$attribute]['$regex'] = new Regex(".*{$this->escapeWildcards($value)}.*", 'i'); } else { $filter[$attribute]['$in'] = $query->getValues(); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index fbc1aaa44..af8a15c89 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -876,7 +876,7 @@ public function createDocument(string $collection, Document $document): Document $queryPermissions = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $queryPermissions); $stmtPermissions = $this->getPDO()->prepare($queryPermissions); - if($sqlTenant) { + if ($sqlTenant) { $stmtPermissions->bindValue(':_tenant', $this->tenant); } } @@ -935,7 +935,7 @@ public function createDocuments(string $collection, array $documents, int $batch $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = \json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -1768,7 +1768,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -1896,7 +1896,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) $queries = array_map(fn ($query) => clone $query, $queries); $conditions = $this->getSQLConditions($queries); - if(!empty($conditions)) { + if (!empty($conditions)) { $where[] = $conditions; } @@ -2136,7 +2136,7 @@ protected function getFulltextValue(string $value): string */ protected function getSQLType(string $type, int $size, bool $signed = true, bool $array = false): string { - if($array === true) { + if ($array === true) { return 'JSONB'; } diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 567b0f6c1..b06492f3f 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -828,14 +828,14 @@ protected function bindConditionValue(mixed $stmt, Query $query): void return; } - if($query->isNested()) { + if ($query->isNested()) { foreach ($query->getValues() as $value) { $this->bindConditionValue($stmt, $value); } return; } - if($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { + if ($this->getSupportForJSONOverlaps() && $query->onArray() && $query->getMethod() == Query::TYPE_CONTAINS) { $placeholder = $this->getSQLPlaceholder($query) . '_0'; $stmt->bindValue($placeholder, json_encode($query->getValues()), PDO::PARAM_STR); return; @@ -1085,7 +1085,7 @@ public function getSQLConditions(array $queries = [], string $separator = 'AND') continue; } - if($query->isNested()) { + if ($query->isNested()) { $conditions[] = $this->getSQLConditions($query->getValues(), $query->getMethod()); } else { $conditions[] = $this->getSQLCondition($query); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 27d7379ef..55494fa55 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -460,7 +460,7 @@ public function createDocument(string $collection, Document $document): Document $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -534,7 +534,7 @@ public function createDocument(string $collection, Document $document): Document $stmtPermissions = $this->getPDO()->prepare($queryPermissions); - if($this->sharedTables) { + if ($this->sharedTables) { $stmtPermissions->bindValue(':_tenant', $this->tenant); } } @@ -579,7 +579,7 @@ public function updateDocument(string $collection, string $id, Document $documen $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); - if($this->sharedTables) { + if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; } @@ -713,7 +713,7 @@ public function updateDocument(string $collection, string $id, Document $documen $stmtAddPermissions = $this->getPDO()->prepare($sql); $stmtAddPermissions->bindValue(":_uid", $document->getId()); - if($this->sharedTables) { + if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } diff --git a/src/Database/Exception.php b/src/Database/Exception.php index 64ad3c997..94099c6ae 100644 --- a/src/Database/Exception.php +++ b/src/Database/Exception.php @@ -8,7 +8,7 @@ class Exception extends \Exception { public function __construct(string $message, int|string $code = 0, Throwable $previous = null) { - if(\is_string($code)) { + if (\is_string($code)) { if (\is_numeric($code)) { $code = (int) $code; } else { diff --git a/src/Database/Query.php b/src/Database/Query.php index 6af553415..aab17c195 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -297,7 +297,7 @@ public function toArray(): array { $array = ['method' => $this->method]; - if(!empty($this->attribute)) { + if (!empty($this->attribute)) { $array['attribute'] = $this->attribute; } @@ -690,7 +690,7 @@ public static function groupByType(array $queries): array */ public function isNested(): bool { - if(in_array($this->getMethod(), self::LOGICAL_TYPES)) { + if (in_array($this->getMethod(), self::LOGICAL_TYPES)) { return true; } diff --git a/src/Database/Validator/Datetime.php b/src/Database/Validator/Datetime.php index e8ffee5ff..812669d7a 100644 --- a/src/Database/Validator/Datetime.php +++ b/src/Database/Validator/Datetime.php @@ -33,7 +33,7 @@ class Datetime extends Validator */ public function __construct(bool $requireDateInFuture = false, string $precision = self::PRECISION_ANY, int $offset = 0) { - if($offset < 0) { + if ($offset < 0) { throw new \Exception('Offset must be a positive number.'); } @@ -50,13 +50,13 @@ public function getDescription(): string { $message = 'Value must be valid date'; - if($this->offset > 0) { + if ($this->offset > 0) { $message .= " at least " . $this->offset . " seconds in future"; - } elseif($this->requireDateInFuture) { + } elseif ($this->requireDateInFuture) { $message .= " in future"; } - if($this->precision !== self::PRECISION_ANY) { + if ($this->precision !== self::PRECISION_ANY) { $message .= " with " . $this->precision . " precision"; } @@ -84,9 +84,9 @@ public function isValid($value): bool return false; } - if($this->offset !== 0) { + if ($this->offset !== 0) { $diff = $date->getTimestamp() - $now->getTimestamp(); - if($diff <= $this->offset) { + if ($diff <= $this->offset) { return false; } } @@ -109,12 +109,12 @@ public function isValid($value): bool break; } - foreach($denyConstants as $constant) { - if(\intval($date->format($constant)) !== 0) { + foreach ($denyConstants as $constant) { + if (\intval($date->format($constant)) !== 0) { return false; } } - } catch(\Exception $e) { + } catch (\Exception $e) { return false; } diff --git a/src/Database/Validator/Index.php b/src/Database/Validator/Index.php index b24e39f73..fdaa1efe0 100644 --- a/src/Database/Validator/Index.php +++ b/src/Database/Validator/Index.php @@ -126,30 +126,30 @@ public function checkArrayIndex(Document $index): bool foreach ($attributes as $attributePosition => $attributeName) { $attribute = $this->attributes[\strtolower($attributeName)] ?? new Document(); - if($attribute->getAttribute('array', false)) { + if ($attribute->getAttribute('array', false)) { // Database::INDEX_UNIQUE Is not allowed! since mariaDB VS MySQL makes the unique Different on values - if($index->getAttribute('type') != Database::INDEX_KEY) { + if ($index->getAttribute('type') != Database::INDEX_KEY) { $this->message = '"' . ucfirst($index->getAttribute('type')) . '" index is forbidden on array attributes'; return false; } - if(empty($lengths[$attributePosition])) { + if (empty($lengths[$attributePosition])) { $this->message = 'Index length for array not specified'; return false; } $arrayAttributes[] = $attribute->getAttribute('key', ''); - if(count($arrayAttributes) > 1) { + if (count($arrayAttributes) > 1) { $this->message = 'An index may only contain one array attribute'; return false; } $direction = $orders[$attributePosition] ?? ''; - if(!empty($direction)) { + if (!empty($direction)) { $this->message = 'Invalid index order "' . $direction . '" on array attribute "'. $attribute->getAttribute('key', '') .'"'; return false; } - } elseif($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { + } elseif ($attribute->getAttribute('type') !== Database::VAR_STRING && !empty($lengths[$attributePosition])) { $this->message = 'Cannot set a length on "'. $attribute->getAttribute('type') . '" attributes'; return false; } @@ -188,7 +188,7 @@ public function checkIndexLength(Document $index): bool break; } - if($attribute->getAttribute('array', false)) { + if ($attribute->getAttribute('array', false)) { $attributeSize = Database::ARRAY_INDEX_LENGTH; $indexLength = Database::ARRAY_INDEX_LENGTH; } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 2e4aac71a..b1d67aad0 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -71,8 +71,8 @@ public function isValid($value): bool } } - if($query->isNested()) { - if(!self::isValid($query->getValues())) { + if ($query->isNested()) { + if (!self::isValid($query->getValues())) { return false; } } diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 635fa4732..1fb50f9fc 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -131,29 +131,29 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s } } - if($attributeSchema['type'] === 'relationship') { + if ($attributeSchema['type'] === 'relationship') { /** * We can not disable relationship query since we have logic that use it, * so instead we validate against the relation type */ $options = $attributeSchema['options']; - if($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { + if ($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + if ($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + if ($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } - if($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + if ($options['relationType'] === Database::RELATION_MANY_TO_MANY) { $this->message = 'Cannot query on virtual relationship attribute'; return false; } @@ -161,7 +161,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s $array = $attributeSchema['array'] ?? false; - if( + if ( !$array && $method === Query::TYPE_CONTAINS && $attributeSchema['type'] !== Database::VAR_STRING @@ -170,7 +170,7 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s return false; } - if( + if ( $array && !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) ) { @@ -253,12 +253,12 @@ public function isValid($value): bool case Query::TYPE_AND: $filters = Query::groupByType($value->getValues())['filters']; - if(count($value->getValues()) !== count($filters)) { + if (count($value->getValues()) !== count($filters)) { $this->message = \ucfirst($method) . ' queries can only contain filter queries'; return false; } - if(count($filters) < 2) { + if (count($filters) < 2) { $this->message = \ucfirst($method) . ' queries require at least two queries'; return false; } diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 46b1edafd..2e4cfde97 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -311,7 +311,7 @@ protected function checkForInvalidAttributeValues(array $structure, array $keys) continue; } - if($type === Database::VAR_RELATIONSHIP) { + if ($type === Database::VAR_RELATIONSHIP) { continue; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 1ac815705..351665005 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1002,7 +1002,7 @@ public function testQueryTimeout(): void Query::notEqual('longtext', 'appwrite'), ]); $this->fail('Failed to throw exception'); - } catch(TimeoutException $ex) { + } catch (TimeoutException $ex) { static::getDatabase()->clearTimeout(); static::getDatabase()->deleteCollection('global-timeouts'); } @@ -1909,7 +1909,7 @@ public function testCreateDocument(): Document 'empty' => [], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertTrue($e instanceof StructureException); $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage()); } @@ -1928,7 +1928,7 @@ public function testCreateDocument(): Document 'empty' => [], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertTrue($e instanceof StructureException); $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage()); } @@ -2319,7 +2319,7 @@ public function testListDocumentSearch(): void public function testEmptyTenant(): void { - if(static::getDatabase()->getAdapter()->getSharedTables()) { + if (static::getDatabase()->getAdapter()->getSharedTables()) { $this->expectNotToPerformAssertions(); return; } @@ -2464,7 +2464,7 @@ public function testUpdateDocumentConflict(Document $document): void return static::getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); }); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertTrue($e instanceof ConflictException); $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); } @@ -2605,7 +2605,7 @@ public function testArrayAttribute(): void try { $database->createDocument($collection, new Document([])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Missing required attribute "booleans"', $e->getMessage()); } @@ -2625,7 +2625,7 @@ public function testArrayAttribute(): void 'short' => ['More than 5 size'], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "short[\'0\']" has invalid type. Value must be a valid string and no longer than 5 chars', $e->getMessage()); } @@ -2634,7 +2634,7 @@ public function testArrayAttribute(): void 'names' => ['Joe', 100], ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "names[\'1\']" has invalid type. Value must be a valid string and no longer than 255 chars', $e->getMessage()); } @@ -2643,7 +2643,7 @@ public function testArrayAttribute(): void 'age' => 1.5, ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid integer', $e->getMessage()); } @@ -2652,7 +2652,7 @@ public function testArrayAttribute(): void 'age' => -100, ])); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid document structure: Attribute "age" has invalid type. Value must be a valid range between 0 and 2,147,483,647', $e->getMessage()); } @@ -2681,7 +2681,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { if (static::getDatabase()->getAdapter()->getSupportForFulltextIndex()) { $this->assertEquals('"Fulltext" index is forbidden on array attributes', $e->getMessage()); } else { @@ -2692,7 +2692,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx', Database::INDEX_KEY, ['numbers', 'names'], [100,100]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('An index may only contain one array attribute', $e->getMessage()); } @@ -2713,7 +2713,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx_numbers', Database::INDEX_KEY, ['tv_show', 'numbers'], [], []); // [700, 255] $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Index length is longer than the maximum: 768', $e->getMessage()); } } @@ -2724,7 +2724,7 @@ public function testArrayAttribute(): void try { $database->createIndex($collection, 'indx4', Database::INDEX_KEY, ['age', 'names'], [10, 255], []); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Cannot set a length on "integer" attributes', $e->getMessage()); } @@ -2737,7 +2737,7 @@ public function testArrayAttribute(): void Query::equal('names', ['Joe']), ]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid query: Cannot query equal on attribute "names" because it is an array.', $e->getMessage()); } @@ -2746,7 +2746,7 @@ public function testArrayAttribute(): void Query::contains('age', [10]) ]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid query: Cannot query contains on attribute "age" because it is not an array or string.', $e->getMessage()); } @@ -3182,7 +3182,7 @@ public function testFindContains(): void Query::contains('price', [10.5]), ]); $this->fail('Failed to throw exception'); - } catch(Throwable $e) { + } catch (Throwable $e) { $this->assertEquals('Invalid query: Cannot query contains on attribute "price" because it is not an array or string.', $e->getMessage()); $this->assertTrue($e instanceof DatabaseException); } @@ -4089,7 +4089,7 @@ public function testOrSingleQuery(): void ]) ]); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertEquals('Invalid query: Or queries require at least two queries', $e->getMessage()); } } @@ -4149,7 +4149,7 @@ public function testAndSingleQuery(): void ]) ]); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertEquals('Invalid query: And queries require at least two queries', $e->getMessage()); } } @@ -4997,7 +4997,7 @@ public function testStructureValidationAfterRelationsAttribute(): void 'name' => 'Frozen', // Unknown attribute 'name' after relation attribute ])); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertInstanceOf(StructureException::class, $e); } } @@ -5072,7 +5072,7 @@ public function testNoChangeUpdateDocumentWithRelationWithoutPermission(): void try { static::getDatabase()->updateDocument('level1', $level1->getId(), $level1->setAttribute('name', 'haha')); $this->fail('Failed to throw exception'); - } catch(Exception $e) { + } catch (Exception $e) { $this->assertInstanceOf(AuthorizationException::class, $e); } $level1->setAttribute('name', 'Level 1'); diff --git a/tests/unit/Validator/DateTimeTest.php b/tests/unit/Validator/DateTimeTest.php index b9157e8b8..d8ababb6f 100644 --- a/tests/unit/Validator/DateTimeTest.php +++ b/tests/unit/Validator/DateTimeTest.php @@ -128,7 +128,7 @@ public function testOffset(): void $threwException = false; try { $dateValidator = new DatetimeValidator(offset: -60); - } catch(\Exception $e) { + } catch (\Exception $e) { $threwException = true; } $this->assertTrue($threwException); From 2626a7463bf5096ebe7a44f7d18e90ce84be261f Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 13 Jan 2025 16:54:41 +1300 Subject: [PATCH 256/256] Sync database --- src/Database/Database.php | 1043 ++++++++++++++++++++++--------------- 1 file changed, 634 insertions(+), 409 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 8957f0072..11a3df434 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -9,6 +9,7 @@ use Utopia\Database\Exception\Conflict as ConflictException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; +use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; @@ -139,17 +140,6 @@ class Database public const INSERT_BATCH_SIZE = 100; public const DELETE_BATCH_SIZE = 100; - protected Adapter $adapter; - - protected Cache $cache; - - protected string $cacheName = 'default'; - - /** - * @var array - */ - protected array $map = []; - /** * List of Internal attributes * @@ -291,6 +281,17 @@ class Database 'indexes' => [], ]; + protected Adapter $adapter; + + protected Cache $cache; + + protected string $cacheName = 'default'; + + /** + * @var array + */ + protected array $map = []; + /** * @var array */ @@ -302,14 +303,14 @@ class Database protected array $instanceFilters = []; /** - * @var array + * @var array> */ protected array $listeners = [ '*' => [], ]; /** - * Array in which the keys are the names of databse listeners that + * Array in which the keys are the names of database listeners that * should be skipped when dispatching events. null $silentListeners * will skip all listeners. * @@ -321,6 +322,8 @@ class Database protected bool $resolveRelationships = true; + protected bool $checkRelationshipsExist = true; + protected int $relationshipFetchDepth = 1; protected bool $filter = true; @@ -329,6 +332,10 @@ class Database protected bool $preserveDates = false; + protected int $maxQueryValues = 100; + + protected bool $migrating = false; + /** * Stack of collection IDs when creating or updating related documents * @var array @@ -350,8 +357,11 @@ class Database * @param Cache $cache * @param array $filters */ - public function __construct(Adapter $adapter, Cache $cache, array $filters = []) - { + public function __construct( + Adapter $adapter, + Cache $cache, + array $filters = [] + ) { $this->adapter = $adapter; $this->cache = $cache; $this->instanceFilters = $filters; @@ -401,13 +411,12 @@ function (mixed $value) { self::addFilter( 'datetime', /** - * @param string|null $value - * @return string|null - * @throws Exception + * @param mixed $value + * @return mixed */ - function (?string $value) { + function (mixed $value) { if (is_null($value)) { - return null; + return; } try { $value = new \DateTime($value); @@ -433,9 +442,9 @@ function (?string $value) { * @param string $event * @param string $name * @param callable $callback - * @return self + * @return static */ - public function on(string $event, string $name, callable $callback): self + public function on(string $event, string $name, callable $callback): static { if (!isset($this->listeners[$event])) { $this->listeners[$event] = []; @@ -453,7 +462,7 @@ public function on(string $event, string $name, callable $callback): self * @param callable $callback * @return $this */ - public function before(string $event, string $name, callable $callback): self + public function before(string $event, string $name, callable $callback): static { $this->adapter->before($event, $name, $callback); @@ -468,7 +477,7 @@ public function before(string $event, string $name, callable $callback): self * @param array|null $listeners List of listeners to silence; if null, all listeners will be silenced * @return T */ - public function silent(callable $callback, array $listeners = null): mixed + public function silent(callable $callback, ?array $listeners = null): mixed { $previous = $this->silentListeners; @@ -489,6 +498,17 @@ public function silent(callable $callback, array $listeners = null): mixed } } + /** + * Get getConnection Id + * + * @return string + * @throws Exception + */ + public function getConnectionId(): string + { + return $this->adapter->getConnectionId(); + } + /** * Skip relationships for all the calls inside the callback * @@ -508,6 +528,18 @@ public function skipRelationships(callable $callback): mixed } } + public function skipRelationshipsExistCheck(callable $callback): mixed + { + $previous = $this->checkRelationshipsExist; + $this->checkRelationshipsExist = false; + + try { + return $callback(); + } finally { + $this->checkRelationshipsExist = $previous; + } + } + /** * Trigger callback for events * @@ -566,7 +598,7 @@ public function withRequestTimestamp(?\DateTime $requestTimestamp, callable $cal * * @throws DatabaseException */ - public function setNamespace(string $namespace): self + public function setNamespace(string $namespace): static { $this->adapter->setNamespace($namespace); @@ -590,10 +622,10 @@ public function getNamespace(): string * * @param string $name * - * @return self + * @return static * @throws DatabaseException */ - public function setDatabase(string $name): self + public function setDatabase(string $name): static { $this->adapter->setDatabase($name); @@ -620,7 +652,7 @@ public function getDatabase(): string * * @return $this */ - public function setCache(Cache $cache): self + public function setCache(Cache $cache): static { $this->cache = $cache; return $this; @@ -642,7 +674,7 @@ public function getCache(): Cache * @param string $name * @return $this */ - public function setCacheName(string $name): self + public function setCacheName(string $name): static { $this->cacheName = $name; @@ -664,9 +696,9 @@ public function getCacheName(): string * * @param string $key * @param mixed $value - * @return self + * @return static */ - public function setMetadata(string $key, mixed $value): self + public function setMetadata(string $key, mixed $value): static { $this->adapter->setMetadata($key, $value); @@ -698,10 +730,10 @@ public function resetMetadata(): void * * @param int $milliseconds * @param string $event - * @return self + * @return static * @throws Exception */ - public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): self + public function setTimeout(int $milliseconds, string $event = Database::EVENT_ALL): static { $this->adapter->setTimeout($milliseconds, $event); @@ -724,10 +756,9 @@ public function clearTimeout(string $event = Database::EVENT_ALL): void * * @return $this */ - public function enableFilters(): self + public function enableFilters(): static { $this->filter = true; - return $this; } @@ -736,13 +767,33 @@ public function enableFilters(): self * * @return $this */ - public function disableFilters(): self + public function disableFilters(): static { $this->filter = false; - return $this; } + /** + * Skip filters + * + * Execute a callback without filters + * + * @template T + * @param callable(): T $callback + * @return T + */ + public function skipFilters(callable $callback): mixed + { + $initial = $this->filter; + $this->disableFilters(); + + try { + return $callback(); + } finally { + $this->filter = $initial; + } + } + /** * Get instance filters * @@ -758,7 +809,7 @@ public function getInstanceFilters(): array * * @return $this */ - public function enableValidation(): self + public function enableValidation(): static { $this->validate = true; @@ -770,7 +821,7 @@ public function enableValidation(): self * * @return $this */ - public function disableValidation(): self + public function disableValidation(): static { $this->validate = false; @@ -780,7 +831,7 @@ public function disableValidation(): self /** * Skip Validation * - * Skips validation for the code to be executed inside the callback + * Execute a callback without validation * * @template T * @param callable(): T $callback @@ -799,14 +850,25 @@ public function skipValidation(callable $callback): mixed } /** - * Set Share Tables + * Get shared tables + * + * Get whether to share tables between tenants + * @return bool + */ + public function getSharedTables(): bool + { + return $this->adapter->getSharedTables(); + } + + /** + * Set shard tables * * Set whether to share tables between tenants * * @param bool $sharedTables - * @return self + * @return static */ - public function setSharedTables(bool $sharedTables): self + public function setSharedTables(bool $sharedTables): static { $this->adapter->setSharedTables($sharedTables); @@ -819,22 +881,96 @@ public function setSharedTables(bool $sharedTables): self * Set tenant to use if tables are shared * * @param ?int $tenant - * @return self + * @return static */ - public function setTenant(?int $tenant): self + public function setTenant(?int $tenant): static { $this->adapter->setTenant($tenant); return $this; } - public function setPreserveDates(bool $preserve): self + /** + * Get Tenant + * + * Get tenant to use if tables are shared + * + * @return ?int + */ + public function getTenant(): ?int + { + return $this->adapter->getTenant(); + } + + /** + * With Tenant + * + * Execute a callback with a specific tenant + * + * @param int|null $tenant + * @param callable $callback + * @return mixed + */ + public function withTenant(?int $tenant, callable $callback): mixed + { + $previous = $this->adapter->getTenant(); + $this->adapter->setTenant($tenant); + + try { + return $callback(); + } finally { + $this->adapter->setTenant($previous); + } + } + + public function getPreserveDates(): bool + { + return $this->preserveDates; + } + + public function setPreserveDates(bool $preserve): static { $this->preserveDates = $preserve; return $this; } + public function setMigrating(bool $migrating): self + { + $this->migrating = $migrating; + + return $this; + } + + public function isMigrating(): bool + { + return $this->migrating; + } + + public function withPreserveDates(callable $callback): mixed + { + $previous = $this->preserveDates; + $this->preserveDates = true; + + try { + return $callback(); + } finally { + $this->preserveDates = $previous; + } + } + + public function setMaxQueryValues(int $max): self + { + $this->maxQueryValues = $max; + + return $this; + } + + public function getMaxQueryValues(): int + { + return$this->maxQueryValues; + } + /** * Get list of keywords that cannot be used * @@ -921,17 +1057,16 @@ public function ping(): bool /** * Create the database * - * @throws DatabaseException - * + * @param string|null $database * @return bool + * @throws DuplicateException + * @throws LimitException + * @throws Exception */ public function create(?string $database = null): bool { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } + $database ??= $this->adapter->getDatabase(); - $database = $database ?? $this->adapter->getDatabase(); $this->adapter->create($database); /** @@ -960,11 +1095,7 @@ public function create(?string $database = null): bool */ public function exists(?string $database = null, ?string $collection = null): bool { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - - $database = $database ?? $this->adapter->getDatabase(); + $database ??= $this->adapter->getDatabase(); return $this->adapter->exists($database, $collection); } @@ -976,10 +1107,6 @@ public function exists(?string $database = null, ?string $collection = null): bo */ public function list(): array { - if ($this->adapter->getSharedTables() && 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); @@ -992,13 +1119,10 @@ public function list(): array * * @param string|null $database * @return bool + * @throws DatabaseException */ public function delete(?string $database = null): bool { - if ($this->adapter->getSharedTables() && 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); @@ -1008,6 +1132,8 @@ public function delete(?string $database = null): bool 'deleted' => $deleted ]); + $this->cache->flush(); + return $deleted; } @@ -1017,19 +1143,15 @@ public function delete(?string $database = null): bool * @param string $id * @param array $attributes * @param array $indexes - * @param array $permissions + * @param array|null $permissions * @param bool $documentSecurity * @return Document * @throws DatabaseException * @throws DuplicateException * @throws LimitException */ - public function createCollection(string $id, array $attributes = [], array $indexes = [], array $permissions = null, bool $documentSecurity = true): Document + public function createCollection(string $id, array $attributes = [], array $indexes = [], ?array $permissions = null, bool $documentSecurity = true): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - $permissions ??= [ Permission::create(Role::any()), ]; @@ -1059,7 +1181,8 @@ public function createCollection(string $id, array $attributes = [], array $inde if ($this->validate) { $validator = new IndexValidator( $attributes, - $this->adapter->getMaxIndexLength() + $this->adapter->getMaxIndexLength(), + $this->adapter->getInternalIndexesKeys() ); foreach ($indexes as $index) { if (!$validator->isValid($index)) { @@ -1068,12 +1191,6 @@ public function createCollection(string $id, array $attributes = [], array $inde } } - $this->adapter->createCollection($id, $attributes, $indexes); - - if ($id === self::METADATA) { - return new Document(self::COLLECTION); - } - // Check index limits, if given if ($indexes && $this->adapter->getCountOfIndexes($collection) > $this->adapter->getLimitForIndexes()) { throw new LimitException('Index limit of ' . $this->adapter->getLimitForIndexes() . ' exceeded. Cannot create collection.'); @@ -1085,17 +1202,23 @@ public function createCollection(string $id, array $attributes = [], array $inde $this->adapter->getLimitForAttributes() > 0 && $this->adapter->getCountOfAttributes($collection) > $this->adapter->getLimitForAttributes() ) { - throw new LimitException('Column limit of ' . $this->adapter->getLimitForAttributes() . ' exceeded. Cannot create collection.'); + throw new LimitException('Attribute limit of ' . $this->adapter->getLimitForAttributes() . ' exceeded. Cannot create collection.'); } if ( $this->adapter->getDocumentSizeLimit() > 0 && $this->adapter->getAttributeWidth($collection) > $this->adapter->getDocumentSizeLimit() ) { - throw new LimitException('Row width limit of ' . $this->adapter->getDocumentSizeLimit() . ' exceeded. Cannot create collection.'); + throw new LimitException('Document size limit of ' . $this->adapter->getDocumentSizeLimit() . ' exceeded. Cannot create collection.'); } } + $this->adapter->createCollection($id, $attributes, $indexes); + + if ($id === self::METADATA) { + return new Document(self::COLLECTION); + } + $createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection)); $this->trigger(self::EVENT_COLLECTION_CREATE, $createdCollection); @@ -1116,10 +1239,6 @@ public function createCollection(string $id, array $attributes = [], array $inde */ public function updateCollection(string $id, array $permissions, bool $documentSecurity): Document { - if ($this->adapter->getSharedTables() && 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)) { @@ -1130,14 +1249,14 @@ public function updateCollection(string $id, array $permissions, bool $documentS $collection = $this->silent(fn () => $this->getCollection($id)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ( $this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant() ) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $collection @@ -1161,16 +1280,15 @@ public function updateCollection(string $id, array $permissions, bool $documentS */ public function getCollection(string $id): Document { - if ($this->adapter->getSharedTables() && 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)); + $tenant = $collection->getAttribute('$tenant'); + if ( $id !== self::METADATA && $this->adapter->getSharedTables() - && $collection->getAttribute('$tenant') != $this->adapter->getTenant() + && $tenant !== null + && $tenant != $this->adapter->getTenant() ) { return new Document(); } @@ -1191,60 +1309,47 @@ public function getCollection(string $id): Document */ public function listCollections(int $limit = 25, int $offset = 0): array { - if ($this->adapter->getSharedTables() && 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->getSharedTables()) { - $result = \array_filter($result, function ($collection) { - return $collection->getAttribute('$tenant') == $this->adapter->getTenant(); - }); - } - $this->trigger(self::EVENT_COLLECTION_LIST, $result); return $result; } /** - * Get Collection Size on disk + * Get Collection Size * * @param string $collection * * @return int + * @throws Exception */ - public function getSizeOfCollectionOnDisk(string $collection): int + public function getSizeOfCollection(string $collection): int { - if ($this->adapter->getSharedTables() && 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'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } - return $this->adapter->getSizeOfCollectionOnDisk($collection->getId()); + return $this->adapter->getSizeOfCollection($collection->getId()); } /** - * Get Collection Size of Data + * Get Collection Size on disk * * @param string $collection * * @return int */ - public function getSizeOfCollection(string $collection): int + public function getSizeOfCollectionOnDisk(string $collection): int { if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); @@ -1253,14 +1358,14 @@ public function getSizeOfCollection(string $collection): int $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } - return $this->adapter->getSizeOfCollection($collection->getId()); + return $this->adapter->getSizeOfCollectionOnDisk($collection->getId()); } /** @@ -1273,24 +1378,20 @@ public function getSizeOfCollection(string $collection): int */ public function deleteCollection(string $id): bool { - if ($this->adapter->getSharedTables() && 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'); + throw new NotFoundException('Collection not found'); } if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $relationships = \array_filter( $collection->getAttribute('attributes'), fn ($attribute) => - $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP ); foreach ($relationships as $relationship) { @@ -1309,6 +1410,8 @@ public function deleteCollection(string $id): bool $this->trigger(self::EVENT_COLLECTION_DELETE, $collection); } + $this->purgeCachedCollection($id); + return $deleted; } @@ -1334,24 +1437,17 @@ public function deleteCollection(string $id): bool * @throws DuplicateException * @throws LimitException * @throws StructureException + * @throws Exception */ - 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 + 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->getSharedTables() && 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'); + throw new NotFoundException('Collection not found'); } - if ($this->adapter->getSharedTables() && $collection->getAttribute('$tenant') != $this->adapter->getTenant()) { - throw new DatabaseException('Collection not found'); - } - - // Attribute IDs are case insensitive + // Attribute IDs are case-insensitive $attributes = $collection->getAttribute('attributes', []); /** @var array $attributes */ foreach ($attributes as $attribute) { @@ -1360,9 +1456,9 @@ public function createAttribute(string $collection, string $id, string $type, in } } - /** Ensure required filters for the attribute are passed */ + // Ensure required filters for the attribute are passed $requiredFilters = $this->getRequiredFilters($type); - if (!empty(array_diff($requiredFilters, $filters))) { + if (!empty(\array_diff($requiredFilters, $filters))) { throw new DatabaseException("Attribute of type: $type requires the following filters: " . implode(",", $requiredFilters)); } @@ -1424,10 +1520,10 @@ public function createAttribute(string $collection, string $id, string $type, in throw new DatabaseException('Unknown attribute type: ' . $type . '. Must be one of ' . self::VAR_STRING . ', ' . self::VAR_INTEGER . ', ' . self::VAR_FLOAT . ', ' . self::VAR_BOOLEAN . ', ' . self::VAR_DATETIME . ', ' . self::VAR_RELATIONSHIP); } - // only execute when $default is given + // Only execute when $default is given if (!\is_null($default)) { if ($required === true) { - throw new DatabaseException('Cannot set a default value on a required attribute'); + throw new DatabaseException('Cannot set a default value for a required attribute'); } $this->validateDefaultTypes($type, $default); @@ -1441,7 +1537,7 @@ public function createAttribute(string $collection, string $id, string $type, in } } catch (DuplicateException $e) { // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. - if (!$this->adapter->getSharedTables()) { + if (!$this->adapter->getSharedTables() || !$this->isMigrating()) { throw $e; } } @@ -1479,7 +1575,7 @@ protected function getRequiredFilters(?string $type): array * @param string $type Type of the attribute * @param mixed $default Default value of the attribute * - * @throws Exception + * @throws DatabaseException * @return void */ protected function validateDefaultTypes(string $type, mixed $default): void @@ -1530,10 +1626,6 @@ protected function validateDefaultTypes(string $type, mixed $default): void */ protected function updateIndexMeta(string $collection, string $id, callable $updateCallback): Document { - if ($this->adapter->getSharedTables() && 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) { @@ -1544,7 +1636,7 @@ protected function updateIndexMeta(string $collection, string $id, callable $upd $index = \array_search($id, \array_map(fn ($index) => $index['$id'], $indexes)); if ($index === false) { - throw new DatabaseException('Index not found'); + throw new NotFoundException('Index not found'); } // Execute update from callback @@ -1573,10 +1665,6 @@ protected function updateIndexMeta(string $collection, string $id, callable $upd */ protected function updateAttributeMeta(string $collection, string $id, callable $updateCallback): Document { - if ($this->adapter->getSharedTables() && 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) { @@ -1587,14 +1675,14 @@ protected function updateAttributeMeta(string $collection, string $id, callable $index = \array_search($id, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); if ($index === false) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } // Execute update from callback $updateCallback($attributes[$index], $collection, $index); // Save - $collection->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN); + $collection->setAttribute('attributes', $attributes); $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); @@ -1700,7 +1788,7 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de /** * Update Attribute. This method is for updating data that causes underlying structure to change. Check out other updateAttribute methods if you are looking for metadata adjustments. - * To update attribute key (ID), use renameAttribute instead. + * * @param string $collection * @param string $id * @param string|null $type @@ -1716,7 +1804,7 @@ public function updateAttributeDefault(string $collection, string $id, mixed $de * @return Document * @throws Exception */ - public function updateAttribute(string $collection, string $id, string $type = null, int $size = null, bool $required = null, mixed $default = null, bool $signed = null, bool $array = null, string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document + public function updateAttribute(string $collection, string $id, ?string $type = null, ?int $size = null, ?bool $required = null, mixed $default = null, ?bool $signed = null, ?bool $array = null, ?string $format = null, ?array $formatOptions = null, ?array $filters = null, ?string $newKey = null): Document { return $this->updateAttributeMeta($collection, $id, function ($attribute, $collectionDoc, $attributeIndex) use ($collection, $id, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters, $newKey) { $altering = !\is_null($type) @@ -1807,15 +1895,13 @@ public function updateAttribute(string $collection, string $id, string $type = n $this->adapter->getDocumentSizeLimit() > 0 && $this->adapter->getAttributeWidth($collectionDoc) >= $this->adapter->getDocumentSizeLimit() ) { - throw new LimitException('Row width limit reached. Cannot create new attribute.'); + throw new LimitException('Row width limit reached. Cannot update attribute.'); } if ($altering) { - $updated = $this->adapter->updateAttribute($collection, $id, $type, $size, $signed, $array, $newKey); - - if ($id !== $newKey) { - $indexes = $collectionDoc->getAttribute('indexes'); + $indexes = $collectionDoc->getAttribute('indexes'); + if (!\is_null($newKey) && $id !== $newKey) { foreach ($indexes as $index) { if (in_array($id, $index['attributes'])) { $index['attributes'] = array_map(function ($attribute) use ($id, $newKey) { @@ -1825,6 +1911,25 @@ public function updateAttribute(string $collection, string $id, string $type = n } } + /** + * Since we allow changing type & size we need to validate index length + */ + if ($this->validate) { + $validator = new IndexValidator( + $attributes, + $this->adapter->getMaxIndexLength(), + $this->adapter->getInternalIndexesKeys() + ); + + foreach ($indexes as $index) { + if (!$validator->isValid($index)) { + throw new DatabaseException($validator->getDescription()); + } + } + } + + $updated = $this->adapter->updateAttribute($collection, $id, $type, $size, $signed, $array, $newKey); + if (!$updated) { throw new DatabaseException('Failed to update attribute'); } @@ -1849,10 +1954,6 @@ public function updateAttribute(string $collection, string $id, string $type = n */ public function checkAttribute(Document $collection, Document $attribute): bool { - if ($this->adapter->getSharedTables() && 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); @@ -1886,10 +1987,6 @@ public function checkAttribute(Document $collection, Document $attribute): bool */ public function deleteAttribute(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -1905,7 +2002,7 @@ public function deleteAttribute(string $collection, string $id): bool } if (\is_null($attribute)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } if ($attribute['type'] === self::VAR_RELATIONSHIP) { @@ -1960,43 +2057,47 @@ public function deleteAttribute(string $collection, string $id): bool */ public function renameAttribute(string $collection, string $old, string $new): bool { - if ($this->adapter->getSharedTables() && 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)); + + /** + * @var array $attributes + */ $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); - $attribute = \in_array($old, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); + /** + * @var array $indexes + */ + $indexes = $collection->getAttribute('indexes', []); - if ($attribute === false) { - throw new DatabaseException('Attribute not found'); - } + $attribute = new Document(); - $attributeNew = \in_array($new, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); + foreach ($attributes as $value) { + if ($value->getId() === $old) { + $attribute = $value; + } - if ($attributeNew !== false) { - throw new DuplicateException('Attribute name already used'); + if ($value->getId() === $new) { + throw new DuplicateException('Attribute name already used'); + } } - foreach ($attributes as $key => $value) { - if (isset($value['$id']) && $value['$id'] === $old) { - $attributes[$key]['key'] = $new; - $attributes[$key]['$id'] = $new; - $attributeNew = $attributes[$key]; - break; - } + if ($attribute->isEmpty()) { + throw new NotFoundException('Attribute not found'); } + $attribute->setAttribute('$id', $new); + $attribute->setAttribute('key', $new); + foreach ($indexes as $index) { $indexAttributes = $index->getAttribute('attributes', []); - $indexAttributes = \array_map(fn ($attribute) => ($attribute === $old) ? $new : $attribute, $indexAttributes); + $indexAttributes = \array_map(fn ($attr) => ($attr === $old) ? $new : $attr, $indexAttributes); $index->setAttribute('attributes', $indexAttributes); } + $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new); + $collection->setAttribute('attributes', $attributes); $collection->setAttribute('indexes', $indexes); @@ -2004,9 +2105,7 @@ public function renameAttribute(string $collection, string $old, string $new): b $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $renamed = $this->adapter->renameAttribute($collection->getId(), $old, $new); - - $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attributeNew); + $this->trigger(self::EVENT_ATTRIBUTE_UPDATE, $attribute); return $renamed; } @@ -2038,20 +2137,16 @@ public function createRelationship( ?string $twoWayKey = null, string $onDelete = Database::RELATION_MUTATE_RESTRICT ): bool { - if ($this->adapter->getSharedTables() && 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'); + throw new NotFoundException('Collection not found'); } $relatedCollection = $this->silent(fn () => $this->getCollection($relatedCollection)); if ($relatedCollection->isEmpty()) { - throw new DatabaseException('Related collection not found'); + throw new NotFoundException('Related collection not found'); } $id ??= $relatedCollection->getId(); @@ -2230,10 +2325,6 @@ public function updateRelationship( ?bool $twoWay = null, ?string $onDelete = null ): bool { - if ($this->adapter->getSharedTables() && 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) @@ -2253,7 +2344,20 @@ public function updateRelationship( throw new DuplicateException('Attribute already exists'); } - $this->updateAttributeMeta($collection->getId(), $id, function ($attribute) use ($collection, $id, $newKey, $newTwoWayKey, $twoWay, $onDelete) { + $attributeIndex = array_search($id, array_map(fn ($attribute) => $attribute['$id'], $attributes)); + + if ($attributeIndex === false) { + throw new NotFoundException('Attribute not found'); + } + + $attribute = $attributes[$attributeIndex]; + $type = $attribute['options']['relationType']; + $side = $attribute['options']['side']; + + $relatedCollectionId = $attribute['options']['relatedCollection']; + $relatedCollection = $this->getCollection($relatedCollectionId); + + $this->updateAttributeMeta($collection->getId(), $id, function ($attribute) use ($collection, $id, $newKey, $newTwoWayKey, $twoWay, $onDelete, $type, $side) { $altering = (!\is_null($newKey) && $newKey !== $id) || (!\is_null($newTwoWayKey) && $newTwoWayKey !== $attribute['options']['twoWayKey']); @@ -2268,9 +2372,6 @@ public function updateRelationship( throw new DuplicateException('Related attribute already exists'); } - $type = $attribute['options']['relationType']; - $side = $attribute['options']['side']; - $newKey ??= $attribute['key']; $twoWayKey = $attribute['options']['twoWayKey']; $newTwoWayKey ??= $attribute['options']['twoWayKey']; @@ -2332,68 +2433,76 @@ public function updateRelationship( throw new DatabaseException('Failed to update relationship'); } } + }); - $this->purgeCachedCollection($collection->getId()); - $this->purgeCachedCollection($relatedCollection->getId()); + // Update Indexes + $renameIndex = function (string $collection, string $key, string $newKey) { + $this->updateIndexMeta( + $collection, + '_index_' . $key, + function ($index) use ($newKey) { + $index->setAttribute('attributes', [$newKey]); + } + ); + $this->silent( + fn () => + $this->renameIndex($collection, '_index_' . $key, '_index_' . $newKey) + ); + }; - $renameIndex = function (string $collection, string $key, string $newKey) { - $this->updateIndexMeta( - $collection, - '_index_' . $key, - fn ($index) => - $index->setAttribute('attributes', [$newKey]) - ); - $this->silent( - fn () => - $this->renameIndex($collection, '_index_' . $key, '_index_' . $newKey) - ); - }; + $newKey ??= $attribute['key']; + $twoWayKey = $attribute['options']['twoWayKey']; + $newTwoWayKey ??= $attribute['options']['twoWayKey']; + $twoWay ??= $attribute['options']['twoWay']; + $onDelete ??= $attribute['options']['onDelete']; - switch ($type) { - case self::RELATION_ONE_TO_ONE: - if ($id !== $newKey) { - $renameIndex($collection->getId(), $id, $newKey); - } - if ($twoWay && $twoWayKey !== $newTwoWayKey) { + switch ($type) { + case self::RELATION_ONE_TO_ONE: + if ($id !== $newKey) { + $renameIndex($collection->getId(), $id, $newKey); + } + if ($twoWay && $twoWayKey !== $newTwoWayKey) { + $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); + } + break; + case self::RELATION_ONE_TO_MANY: + if ($side === Database::RELATION_SIDE_PARENT) { + if ($twoWayKey !== $newTwoWayKey) { $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); } - break; - case self::RELATION_ONE_TO_MANY: - if ($side === Database::RELATION_SIDE_PARENT) { - if ($twoWayKey !== $newTwoWayKey) { - $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); - } - } else { - if ($id !== $newKey) { - $renameIndex($collection->getId(), $id, $newKey); - } - } - break; - case self::RELATION_MANY_TO_ONE: - if ($side === Database::RELATION_SIDE_PARENT) { - if ($id !== $newKey) { - $renameIndex($collection->getId(), $id, $newKey); - } - } else { - if ($twoWayKey !== $newTwoWayKey) { - $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); - } + } else { + if ($id !== $newKey) { + $renameIndex($collection->getId(), $id, $newKey); } - break; - case self::RELATION_MANY_TO_MANY: - $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); - + } + break; + case self::RELATION_MANY_TO_ONE: + if ($side === Database::RELATION_SIDE_PARENT) { if ($id !== $newKey) { - $renameIndex($junction, $id, $newKey); + $renameIndex($collection->getId(), $id, $newKey); } + } else { if ($twoWayKey !== $newTwoWayKey) { - $renameIndex($junction, $twoWayKey, $newTwoWayKey); + $renameIndex($relatedCollection->getId(), $twoWayKey, $newTwoWayKey); } - break; - default: - throw new RelationshipException('Invalid relationship type.'); - } - }); + } + break; + case self::RELATION_MANY_TO_MANY: + $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); + + if ($id !== $newKey) { + $renameIndex($junction, $id, $newKey); + } + if ($twoWayKey !== $newTwoWayKey) { + $renameIndex($junction, $twoWayKey, $newTwoWayKey); + } + break; + default: + throw new RelationshipException('Invalid relationship type.'); + } + + $this->purgeCachedCollection($collection->getId()); + $this->purgeCachedCollection($relatedCollection->getId()); return true; } @@ -2412,10 +2521,6 @@ public function updateRelationship( */ public function deleteRelationship(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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; @@ -2429,7 +2534,7 @@ public function deleteRelationship(string $collection, string $id): bool } if (\is_null($relationship)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $collection->setAttribute('attributes', \array_values($attributes)); @@ -2540,10 +2645,6 @@ public function deleteRelationship(string $collection, string $id): bool */ public function renameIndex(string $collection, string $old, string $new): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -2551,7 +2652,7 @@ public function renameIndex(string $collection, string $old, string $new): bool $index = \in_array($old, \array_map(fn ($index) => $index['$id'], $indexes)); if ($index === false) { - throw new DatabaseException('Index not found'); + throw new NotFoundException('Index not found'); } $indexNew = \in_array($new, \array_map(fn ($index) => $index['$id'], $indexes)); @@ -2603,10 +2704,6 @@ 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->getSharedTables() && 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'); } @@ -2656,6 +2753,16 @@ public function createIndex(string $collection, string $id, string $type, array foreach ($attributes as $i => $attr) { foreach ($collectionAttributes as $collectionAttribute) { if ($collectionAttribute->getAttribute('key') === $attr) { + + /** + * mysql does not save length in collection when length = attributes size + */ + if ($collectionAttribute->getAttribute('type') === Database::VAR_STRING) { + if (!empty($lengths[$i]) && $lengths[$i] === $collectionAttribute->getAttribute('size') && $this->adapter->getMaxIndexLength() > 0) { + $lengths[$i] = null; + } + } + $isArray = $collectionAttribute->getAttribute('array', false); if ($isArray) { if ($this->adapter->getMaxIndexLength() > 0) { @@ -2682,7 +2789,8 @@ public function createIndex(string $collection, string $id, string $type, array if ($this->validate) { $validator = new IndexValidator( $collection->getAttribute('attributes', []), - $this->adapter->getMaxIndexLength() + $this->adapter->getMaxIndexLength(), + $this->adapter->getInternalIndexesKeys() ); if (!$validator->isValid($index)) { throw new DatabaseException($validator->getDescription()); @@ -2697,7 +2805,8 @@ public function createIndex(string $collection, string $id, string $type, array } } catch (DuplicateException $e) { // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. - if (!$this->adapter->getSharedTables()) { + + if (!$this->adapter->getSharedTables() || !$this->isMigrating()) { throw $e; } } @@ -2725,10 +2834,6 @@ public function createIndex(string $collection, string $id, string $type, array */ public function deleteIndex(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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', []); @@ -2741,14 +2846,14 @@ public function deleteIndex(string $collection, string $id): bool } } + $deleted = $this->adapter->deleteIndex($collection->getId(), $id); + $collection->setAttribute('indexes', \array_values($indexes)); if ($collection->getId() !== self::METADATA) { $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $deleted = $this->adapter->deleteIndex($collection->getId(), $id); - $this->trigger(self::EVENT_INDEX_DELETE, $indexDeleted); return $deleted; @@ -2767,16 +2872,12 @@ public function deleteIndex(string $collection, string $id): bool */ public function getDocument(string $collection, string $id, array $queries = [], bool $forUpdate = false): Document { - if ($this->adapter->getSharedTables() && 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(self::COLLECTION); } if (empty($collection)) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } if (empty($id)) { @@ -2786,7 +2887,7 @@ public function getDocument(string $collection, string $id, array $queries = [], $collection = $this->silent(fn () => $this->getCollection($collection)); if ($collection->isEmpty()) { - throw new DatabaseException('Collection not found'); + throw new NotFoundException('Collection not found'); } $attributes = $collection->getAttribute('attributes', []); @@ -2901,7 +3002,7 @@ public function getDocument(string $collection, string $id, array $queries = [], $relationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => - $attribute['type'] === Database::VAR_RELATIONSHIP + $attribute['type'] === Database::VAR_RELATIONSHIP ); $hasTwoWayRelationship = false; @@ -2914,7 +3015,6 @@ public function getDocument(string $collection, string $id, array $queries = [], /** * Bug with function purity in PHPStan means it thinks $this->map is always empty - * * @phpstan-ignore-next-line */ foreach ($this->map as $key => $value) { @@ -2930,10 +3030,10 @@ public function getDocument(string $collection, string $id, array $queries = [], } } - // Don't save to cache if it's part of a two-way relationship or a relationship at all + // Don't save to cache if it's part of a relationship if (!$hasTwoWayRelationship && empty($relationships)) { $this->cache->save($documentCacheKey, $document->getArrayCopy(), $documentCacheHash); - //add document reference to the collection key + // Add document reference to the collection key $this->cache->save($collectionCacheKey, 'empty', $documentCacheKey); } @@ -2944,7 +3044,7 @@ public function getDocument(string $collection, string $id, array $queries = [], if ($query->getMethod() === Query::TYPE_SELECT) { $values = $query->getValues(); foreach ($this->getInternalAttributes() as $internalAttribute) { - if (!in_array($internalAttribute['$id'], $values)) { + if (!\in_array($internalAttribute['$id'], $values)) { $document->removeAttribute($internalAttribute['$id']); } } @@ -3018,8 +3118,8 @@ private function populateDocumentRelationships(Document $collection, Document $d // collection as an existing relationship, but a different two-way key (the third condition), // or the same two-way key as an existing relationship, but a different key (the fourth condition). $transitive = (($existingKey === $twoWayKey - && $existingCollection === $relatedCollection->getId() - && $existingSide !== $side) + && $existingCollection === $relatedCollection->getId() + && $existingSide !== $side) || ($existingTwoWayKey === $key && $existingRelatedCollection === $collection->getId() && $existingSide !== $side) @@ -3203,7 +3303,11 @@ private function populateDocumentRelationships(Document $collection, Document $d */ public function createDocument(string $collection, Document $document): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { + if ( + $collection !== self::METADATA + && $this->adapter->getSharedTables() + && empty($this->adapter->getTenant()) + ) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } @@ -3240,7 +3344,11 @@ public function createDocument(string $collection, Document $document): Document } } - $structure = new Structure($collection); + $structure = new Structure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$structure->isValid($document)) { throw new StructureException($structure->getDescription()); } @@ -3260,6 +3368,7 @@ public function createDocument(string $collection, Document $document): Document $document = $this->decode($collection, $document); $this->trigger(self::EVENT_DOCUMENT_CREATE, $document); + return $document; } @@ -3278,10 +3387,6 @@ 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->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if (empty($documents)) { return []; } @@ -3302,7 +3407,11 @@ public function createDocuments(string $collection, array $documents, int $batch $document = $this->encode($collection, $document); - $validator = new Structure($collection); + $validator = new Structure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($document)) { throw new StructureException($validator->getDescription()); } @@ -3326,7 +3435,10 @@ public function createDocuments(string $collection, array $documents, int $batch $documents[$key] = $this->decode($collection, $document); } - $this->trigger(self::EVENT_DOCUMENTS_CREATE, $documents); + $this->trigger(self::EVENT_DOCUMENTS_CREATE, new Document([ + '$collection' => $collection->getId(), + 'modified' => count($documents) + ])); return $documents; } @@ -3344,7 +3456,7 @@ private function createDocumentRelationships(Document $collection, Document $doc $relationships = \array_filter( $attributes, fn ($attribute) => - $attribute['type'] === Database::VAR_RELATIONSHIP + $attribute['type'] === Database::VAR_RELATIONSHIP ); $stackCount = count($this->relationshipWriteStack); @@ -3613,7 +3725,7 @@ private function relateDocumentsById( // Get the related document, will be empty on permissions failure $related = $this->skipRelationships(fn () => $this->getDocument($relatedCollection->getId(), $relationId)); - if ($related->isEmpty()) { + if ($related->isEmpty() && $this->checkRelationshipsExist) { return; } @@ -3642,7 +3754,7 @@ private function relateDocumentsById( $junction = $this->getJunctionCollection($collection, $relatedCollection, $side); $this->skipRelationships(fn () => $this->createDocument($junction, new Document([ - $key => $related->getId(), + $key => $relationId, $twoWayKey => $documentId, '$permissions' => [ Permission::read(Role::any()), @@ -3669,10 +3781,6 @@ private function relateDocumentsById( */ public function updateDocument(string $collection, string $id, Document $document): Document { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if (!$id) { throw new DatabaseException('Must define $id attribute'); } @@ -3829,7 +3937,11 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->encode($collection, $document); - $structureValidator = new Structure($collection); + $structureValidator = new Structure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) throw new StructureException($structureValidator->getDescription()); } @@ -3866,24 +3978,48 @@ public function updateDocument(string $collection, string $id, Document $documen * @param array $queries * @param int $batchSize * - * @return int + * @return array * * @throws AuthorizationException * @throws DatabaseException */ - public function updateDocuments(string $collection, Document $updates, array $queries = [], int $batchSize = self::INSERT_BATCH_SIZE): int + public function updateDocuments(string $collection, Document $updates, array $queries = [], int $batchSize = self::INSERT_BATCH_SIZE): array { - if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { - throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); - } - if ($updates->isEmpty()) { - return 0; + return []; } - $queries = Query::groupByType($queries)['filters']; $collection = $this->silent(fn () => $this->getCollection($collection)); + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + if ($this->validate) { + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); + + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + } + + $grouped = Query::groupByType($queries); + $limit = $grouped['limit']; + $cursor = $grouped['cursor']; + + if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } + unset($updates['$id']); unset($updates['$createdAt']); unset($updates['$tenant']); @@ -3895,35 +4031,49 @@ public function updateDocuments(string $collection, Document $updates, array $qu $updates = $this->encode($collection, $updates); // Check new document structure - $validator = new PartialStructure($collection); + $validator = new PartialStructure( + $collection, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); + if (!$validator->isValid($updates)) { throw new StructureException($validator->getDescription()); } - $affected = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates) { + $documents = $this->withTransaction(function () use ($collection, $queries, $batchSize, $updates, $limit, $cursor) { $lastDocument = null; - $totalModified = 0; - $affectedDocumentIds = []; + $documents = []; $documentSecurity = $collection->getAttribute('documentSecurity', false); - $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_UPDATE, $collection->getUpdate())); + $authorization = new Authorization(self::PERMISSION_UPDATE); + $skipAuth = $authorization->isValid($collection->getUpdate()); if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { - throw new AuthorizationException($this->authorization->getDescription()); + throw new AuthorizationException($authorization->getDescription()); } + $originalLimit = $limit; + $lastDocument = $cursor; + // Resolve and update relationships while (true) { - $affectedDocuments = $this->find($collection->getId(), array_merge( + if ($limit && $limit < $batchSize) { + $batchSize = $limit; + } elseif (!empty($limit)) { + $limit -= $batchSize; + } + + $affectedDocuments = $this->silent(fn () => $this->find($collection->getId(), array_merge( + $queries, empty($lastDocument) ? [ Query::limit($batchSize), ] : [ Query::limit($batchSize), Query::cursorAfter($lastDocument), - ], - $queries, - ), forPermission: Database::PERMISSION_UPDATE); + ] + ), forPermission: Database::PERMISSION_UPDATE)); if (empty($affectedDocuments)) { break; @@ -3931,12 +4081,21 @@ public function updateDocuments(string $collection, Document $updates, array $qu foreach ($affectedDocuments as $document) { if ($this->resolveRelationships) { - $newDocument = array_merge($document->getArrayCopy(), $updates->getArrayCopy()); + $newDocument = new Document(array_merge($document->getArrayCopy(), $updates->getArrayCopy())); + $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, $newDocument)); + $documents[] = $newDocument; + } - $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, new Document($newDocument))); + // Check if document was updated after the request timestamp + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); } - $affectedDocumentIds[] = $document->getId(); + if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } } $getResults = fn () => $this->adapter->updateDocuments( @@ -3945,28 +4104,31 @@ public function updateDocuments(string $collection, Document $updates, array $qu $affectedDocuments ); - $result = $skipAuth ? $this->authorization->skip($getResults) : $getResults(); - - $totalModified += $result; + $skipAuth ? $authorization->skip($getResults) : $getResults(); if (count($affectedDocuments) < $batchSize) { break; - } else { - $lastDocument = end($affectedDocuments); + } elseif ($originalLimit && count($documents) == $originalLimit) { + break; } - } - $this->trigger(self::EVENT_DOCUMENTS_UPDATE, $affectedDocumentIds); + $lastDocument = end($affectedDocuments); + } - foreach ($affectedDocumentIds as $id) { - $this->purgeRelatedDocuments($collection, $id); - $this->purgeCachedDocument($collection->getId(), $id); + foreach ($documents as $document) { + $this->purgeRelatedDocuments($collection, $document->getId()); + $this->purgeCachedDocument($collection->getId(), $document->getId()); } - return $totalModified; + $this->trigger(self::EVENT_DOCUMENTS_UPDATE, new Document([ + '$collection' => $collection->getId(), + 'modified' => count($documents) + ])); + + return $documents; }); - return $affected; + return $documents; } /** @@ -4122,7 +4284,7 @@ private function updateDocumentRelationships(Document $collection, Document $old $document->setAttribute($key, $related->getId()); break; } - // no break + // no break case 'NULL': if (!\is_null($oldValue?->getId())) { $oldRelated = $this->skipRelationships( @@ -4380,10 +4542,6 @@ 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->getSharedTables() && 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'); } @@ -4393,7 +4551,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string /* @var $document Document */ $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this - if($document->isEmpty()) { + if ($document->isEmpty()) { return false; } @@ -4414,7 +4572,7 @@ public function increaseDocumentAttribute(string $collection, string $id, string }); if (empty($attr)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $whiteList = [self::VAR_INTEGER, self::VAR_FLOAT]; @@ -4475,10 +4633,6 @@ 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->getSharedTables() && 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'); } @@ -4488,7 +4642,7 @@ public function decreaseDocumentAttribute(string $collection, string $id, string /* @var $document Document */ $document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this - if($document->isEmpty()) { + if ($document->isEmpty()) { return false; } @@ -4509,7 +4663,7 @@ public function decreaseDocumentAttribute(string $collection, string $id, string }); if (empty($attr)) { - throw new DatabaseException('Attribute not found'); + throw new NotFoundException('Attribute not found'); } $whiteList = [self::VAR_INTEGER, self::VAR_FLOAT]; @@ -4566,24 +4720,18 @@ public function decreaseDocumentAttribute(string $collection, string $id, string * @throws ConflictException * @throws DatabaseException * @throws RestrictedException - * @throws StructureException */ public function deleteDocument(string $collection, string $id): bool { - if ($this->adapter->getSharedTables() && 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)); $deleted = $this->withTransaction(function () use ($collection, $id, &$document) { - /* @var $document Document */ $document = Authorization::skip(fn () => $this->silent( fn () => $this->getDocument($collection->getId(), $id, forUpdate: true) )); - if($document->isEmpty()) { + if ($document->isEmpty()) { return false; } @@ -4606,7 +4754,7 @@ public function deleteDocument(string $collection, string $id): bool throw new DatabaseException($e->getMessage(), $e->getCode(), $e); } - if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + if (!\is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { throw new ConflictException('Document was updated after the request timestamp'); } @@ -4690,8 +4838,8 @@ private function deleteDocumentRelationships(Document $collection, Document $doc // collection as an existing relationship, but a different two-way key (the third condition), // or the same two-way key as an existing relationship, but a different key (the fourth condition). $transitive = (($existingKey === $twoWayKey - && $existingCollection === $relatedCollection->getId() - && $existingSide !== $side) + && $existingCollection === $relatedCollection->getId() + && $existingSide !== $side) || ($existingTwoWayKey === $key && $existingRelatedCollection === $collection->getId() && $existingSide !== $side) @@ -5015,48 +5163,84 @@ private function deleteCascade(Document $collection, Document $relatedCollection * @param array $queries * @param int $batchSize * - * @return int + * @return array * * @throws AuthorizationException * @throws DatabaseException * @throws RestrictedException */ - public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): int + public function deleteDocuments(string $collection, array $queries = [], int $batchSize = self::DELETE_BATCH_SIZE): array { if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) { throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.'); } - $queries = Query::groupByType($queries)['filters']; $collection = $this->silent(fn () => $this->getCollection($collection)); - $affectedDocumentIds = []; - $deleted = $this->withTransaction(function () use ($collection, $queries, $batchSize, $affectedDocumentIds) { - $lastDocument = null; + if ($collection->isEmpty()) { + throw new DatabaseException('Collection not found'); + } + + $attributes = $collection->getAttribute('attributes', []); + $indexes = $collection->getAttribute('indexes', []); + + if ($this->validate) { + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime() + ); + + if (!$validator->isValid($queries)) { + throw new QueryException($validator->getDescription()); + } + } + + $grouped = Query::groupByType($queries); + $limit = $grouped['limit']; + $cursor = $grouped['cursor']; + + if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { + throw new DatabaseException("cursor Document must be from the same Collection."); + } + $documents = $this->withTransaction(function () use ($collection, $queries, $batchSize, $limit, $cursor) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_DELETE, $collection->getDelete())); + $authorization = new Authorization(self::PERMISSION_DELETE); + $skipAuth = $authorization->isValid($collection->getDelete()); + $documents = []; if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { - throw new AuthorizationException($this->authorization->getDescription()); + throw new AuthorizationException($authorization->getDescription()); } + $originalLimit = $limit; + $lastDocument = $cursor; + while (true) { - $affectedDocuments = $this->find($collection->getId(), array_merge( + if ($limit && $limit < $batchSize) { + $batchSize = $limit; + } elseif (!empty($limit)) { + $limit -= $batchSize; + } + + $affectedDocuments = $this->silent(fn () => $this->find($collection->getId(), array_merge( + $queries, empty($lastDocument) ? [ Query::limit($batchSize), ] : [ Query::limit($batchSize), Query::cursorAfter($lastDocument), - ], - $queries, - ), forPermission: Database::PERMISSION_DELETE); + ] + ), forPermission: Database::PERMISSION_DELETE)); if (empty($affectedDocuments)) { break; } - $affectedDocumentIds = array_merge($affectedDocumentIds, array_map(fn ($document) => $document->getId(), $affectedDocuments)); + $documents = array_merge($affectedDocuments, $documents); foreach ($affectedDocuments as $document) { // Delete Relationships @@ -5064,28 +5248,45 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); } + // Check if document was updated after the request timestamp + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + if (!\is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } + $this->purgeRelatedDocuments($collection, $document->getId()); $this->purgeCachedDocument($collection->getId(), $document->getId()); } if (count($affectedDocuments) < $batchSize) { break; - } else { - $lastDocument = end($affectedDocuments); + } elseif ($originalLimit && count($documents) == $originalLimit) { + break; } + + $lastDocument = end($affectedDocuments); } - if (empty($affectedDocumentIds)) { - return 0; + if (empty($documents)) { + return []; } - $this->trigger(self::EVENT_DOCUMENTS_DELETE, $affectedDocumentIds); + $this->trigger(self::EVENT_DOCUMENTS_DELETE, new Document([ + '$collection' => $collection->getId(), + 'modified' => count($documents) + ])); + + $this->adapter->deleteDocuments($collection->getId(), array_map(fn ($document) => $document->getId(), $documents)); - // Mass delete using adapter with query - return $this->adapter->deleteDocuments($collection->getId(), $affectedDocumentIds); + return $documents; }); - return $deleted; + return $documents; } /** @@ -5138,24 +5339,27 @@ public function purgeCachedDocument(string $collectionId, string $id): bool * @throws DatabaseException * @throws QueryException * @throws TimeoutException + * @throws Exception */ public function find(string $collection, array $queries = [], string $forPermission = Database::PERMISSION_READ): array { - if ($this->adapter->getSharedTables() && 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'); + throw new NotFoundException('Collection not found'); } $attributes = $collection->getAttribute('attributes', []); $indexes = $collection->getAttribute('indexes', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5163,7 +5367,7 @@ public function find(string $collection, array $queries = [], string $forPermiss $authorization = new Authorization(self::PERMISSION_READ); $documentSecurity = $collection->getAttribute('documentSecurity', false); - $skipAuth = $authorization->isValid($collection->getPermissionsByType($forPermission))); + $skipAuth = $authorization->isValid($collection->getPermissionsByType($forPermission)); if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) { throw new AuthorizationException($authorization->getDescription()); @@ -5256,7 +5460,7 @@ public function find(string $collection, array $queries = [], string $forPermiss $results = $skipAuth ? Authorization::skip($getResults) : $getResults(); - foreach ($results as &$node) { + foreach ($results as &$node) { if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) { $node = $this->silent(fn () => $this->populateDocumentRelationships($collection, $node, $nestedSelections)); } @@ -5326,16 +5530,18 @@ public function findOne(string $collection, array $queries = []): Document */ public function count(string $collection, array $queries = [], ?int $max = null): int { - if ($this->adapter->getSharedTables() && 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', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5372,16 +5578,18 @@ public function count(string $collection, array $queries = [], ?int $max = null) */ public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int { - if ($this->adapter->getSharedTables() && 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', []); if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $validator = new DocumentsValidator( + $attributes, + $indexes, + $this->maxQueryValues, + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + ); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } @@ -5440,12 +5648,12 @@ public function encode(Document $collection, Document $document): Document $filters = $attribute['filters'] ?? []; $value = $document->getAttribute($key); - // continue on optional param with no default + // Continue on optional param with no default if (is_null($value) && is_null($default)) { continue; } - // assign default only if no value provided + // Assign default only if no value provided // False positive "Call to function is_null() with mixed will always evaluate to false" // @phpstan-ignore-next-line if (is_null($value) && !is_null($default)) { @@ -5486,13 +5694,13 @@ public function decode(Document $collection, Document $document, array $selectio $attributes = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => - $attribute['type'] !== self::VAR_RELATIONSHIP + $attribute['type'] !== self::VAR_RELATIONSHIP ); $relationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => - $attribute['type'] === self::VAR_RELATIONSHIP + $attribute['type'] === self::VAR_RELATIONSHIP ); foreach ($relationships as $relationship) { @@ -5625,11 +5833,11 @@ public function casting(Document $collection, Document $document): Document protected function encodeAttribute(string $name, mixed $value, Document $document): mixed { if (!array_key_exists($name, self::$filters) && !array_key_exists($name, $this->instanceFilters)) { - throw new DatabaseException("Filter: {$name} not found"); + throw new NotFoundException("Filter: {$name} not found"); } try { - if (array_key_exists($name, $this->instanceFilters)) { + if (\array_key_exists($name, $this->instanceFilters)) { $value = $this->instanceFilters[$name]['encode']($value, $document, $this); } else { $value = self::$filters[$name]['encode']($value, $document, $this); @@ -5661,7 +5869,7 @@ protected function decodeAttribute(string $name, mixed $value, Document $documen } if (!array_key_exists($name, self::$filters) && !array_key_exists($name, $this->instanceFilters)) { - throw new DatabaseException('Filter not found'); + throw new NotFoundException('Filter not found'); } if (array_key_exists($name, $this->instanceFilters)) { @@ -5760,11 +5968,16 @@ public function getLimitForIndexes(): int * @param array $queries * @return array * @throws QueryException + * @throws Exception */ public static function convertQueries(Document $collection, array $queries): array { $attributes = $collection->getAttribute('attributes', []); + foreach (Database::INTERNAL_ATTRIBUTES as $attribute) { + $attributes[] = new Document($attribute); + } + foreach ($attributes as $attribute) { foreach ($queries as $query) { if ($query->getAttribute() === $attribute->getId()) { @@ -5808,7 +6021,7 @@ private function purgeRelatedDocuments(Document $collection, string $id): void $relationships = \array_filter( $collection->getAttribute('attributes', []), fn ($attribute) => - $attribute['type'] === Database::VAR_RELATIONSHIP + $attribute['type'] === Database::VAR_RELATIONSHIP ); if (empty($relationships)) { @@ -5852,4 +6065,16 @@ public function analyzeCollection(string $collection): bool { return $this->adapter->analyzeCollection($collection); } + + /** + * Get Schema Attributes + * + * @param string $collection + * @return array + * @throws DatabaseException + */ + public function getSchemaAttributes(string $collection): array + { + return $this->adapter->getSchemaAttributes($collection); + } }