diff --git a/composer.lock b/composer.lock index 573407f5b..e2fa0a161 100644 --- a/composer.lock +++ b/composer.lock @@ -513,16 +513,16 @@ }, { "name": "laravel/pint", - "version": "v1.13.5", + "version": "v1.13.6", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "df105cf8ce7a8f0b8a9425ff45cd281a5448e423" + "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/df105cf8ce7a8f0b8a9425ff45cd281a5448e423", - "reference": "df105cf8ce7a8f0b8a9425ff45cd281a5448e423", + "url": "https://api.github.com/repos/laravel/pint/zipball/3e3d2ab01c7d8b484c18e6100ecf53639c744fa7", + "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7", "shasum": "" }, "require": { @@ -533,13 +533,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.34.1", - "illuminate/view": "^10.26.2", - "laravel-zero/framework": "^10.1.2", + "friendsofphp/php-cs-fixer": "^3.38.0", + "illuminate/view": "^10.30.1", + "laravel-zero/framework": "^10.3.0", "mockery/mockery": "^1.6.6", "nunomaduro/larastan": "^2.6.4", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.20.0" + "pestphp/pest": "^2.24.2" }, "bin": [ "builds/pint" @@ -575,7 +575,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2023-10-26T09:26:10+00:00" + "time": "2023-11-07T17:59:57+00:00" }, { "name": "myclabs/deep-copy", @@ -839,16 +839,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.40", + "version": "1.10.44", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d" + "reference": "bf84367c53a23f759513985c54ffe0d0c249825b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93c84b5bf7669920d823631e39904d69b9c7dc5d", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bf84367c53a23f759513985c54ffe0d0c249825b", + "reference": "bf84367c53a23f759513985c54ffe0d0c249825b", "shasum": "" }, "require": { @@ -897,7 +897,7 @@ "type": "tidelift" } ], - "time": "2023-10-30T14:48:31+00:00" + "time": "2023-11-21T16:30:46+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2428,7 +2428,7 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -2475,7 +2475,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.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -2495,16 +2495,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -2533,7 +2533,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -2541,7 +2541,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "utopia-php/cli", diff --git a/docker-compose.yml b/docker-compose.yml index 296cd6095..af5a18be8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: MONGO_INITDB_ROOT_PASSWORD: example mysql: - image: mysql:8.0.31 + image: mysql:8.0.33 container_name: utopia-mysql networks: - database diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index fa59aecd3..03bbb789e 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -246,11 +246,11 @@ abstract public function create(string $name): bool; * Optionally check if collection exists in database * * @param string $database database name - * @param string $collection (optional) collection name + * @param string|null $collection (optional) collection name * * @return bool */ - abstract public function exists(string $database, ?string $collection): bool; + abstract public function exists(string $database, ?string $collection = null): bool; /** * List Databases diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 05d66ef60..4ee36bafe 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -27,7 +27,11 @@ public function create(string $name): bool { $name = $this->filter($name); - $sql = "CREATE DATABASE IF NOT EXISTS `{$name}` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;"; + if ($this->exists($name)) { + return true; + } + + $sql = "CREATE DATABASE `{$name}` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;"; $sql = $this->trigger(Database::EVENT_DATABASE_CREATE, $sql); @@ -642,6 +646,7 @@ public function deleteIndex(string $collection, string $id): bool * @throws Exception * @throws PDOException * @throws DuplicateException + * @throws \Throwable */ public function createDocument(string $collection, Document $document): Document { @@ -654,12 +659,6 @@ public function createDocument(string $collection, Document $document): Document $columns = ''; $columnNames = ''; - try { - $this->getPDO()->beginTransaction(); - } catch (PDOException $e) { - $this->getPDO()->rollBack(); - } - /** * Insert Attributes */ @@ -728,27 +727,28 @@ public function createDocument(string $collection, Document $document): Document } try { + $this->getPDO()->beginTransaction(); $stmt->execute(); - $document['$internalId'] = $this->getDocument($collection, $document->getId())->getInternalId(); + $document['$internalId'] = $this->getDocument($collection, $document->getId(), [Query::select(['$internalId'])])->getInternalId(); if (isset($stmtPermissions)) { $stmtPermissions->execute(); } - } catch (PDOException $e) { + + $this->getPDO()->commit(); + } catch (\Throwable $e) { $this->getPDO()->rollBack(); - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage()); - default: - throw $e; + if($e instanceof PDOException) { + switch ($e->getCode()) { + case 1062: + case 23000: + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); + } } - } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); + throw $e; } return $document; @@ -764,6 +764,7 @@ public function createDocument(string $collection, Document $document): Document * @return array * * @throws DuplicateException + * @throws \Throwable */ public function createDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { @@ -771,9 +772,9 @@ 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)); @@ -841,20 +842,22 @@ public function createDocuments(string $collection, array $documents, int $batch } } - if (!$this->getPDO()->commit()) { - throw new Exception('Failed to commit transaction'); - } - - return $documents; - - } catch (PDOException $e) { + $this->getPDO()->commit(); + } catch (\Throwable $e) { $this->getPDO()->rollBack(); - throw match ($e->getCode()) { - 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), - default => $e, - }; + if($e instanceof PDOException) { + switch ($e->getCode()) { + case 1062: + case 23000: + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); + } + } + + throw $e; } + + return $documents; } /** @@ -866,6 +869,7 @@ public function createDocuments(string $collection, array $documents, int $batch * @throws Exception * @throws PDOException * @throws DuplicateException + * @throws \Throwable */ public function updateDocument(string $collection, Document $document): Document { @@ -905,12 +909,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 */ @@ -1037,6 +1035,8 @@ public function updateDocument(string $collection, Document $document): Document } try { + $this->getPDO()->beginTransaction(); + $stmt->execute(); if (isset($stmtRemovePermissions)) { @@ -1045,20 +1045,20 @@ public function updateDocument(string $collection, Document $document): Document if (isset($stmtAddPermissions)) { $stmtAddPermissions->execute(); } - } catch (PDOException $e) { + + $this->getPDO()->commit(); + } catch (\Throwable $e) { $this->getPDO()->rollBack(); - switch ($e->getCode()) { - case 1062: - case 23000: - throw new DuplicateException('Duplicated document: ' . $e->getMessage()); - default: - throw $e; + if($e instanceof PDOException) { + switch ($e->getCode()) { + case 1062: + case 23000: + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); + } } - } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); + throw $e; } return $document; @@ -1074,6 +1074,7 @@ public function updateDocument(string $collection, Document $document): Document * @return array * * @throws DuplicateException + * @throws \Throwable */ public function updateDocuments(string $collection, array $documents, int $batchSize = Database::INSERT_BATCH_SIZE): array { @@ -1081,9 +1082,9 @@ public function updateDocuments(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)); @@ -1264,19 +1265,22 @@ public function updateDocuments(string $collection, array $documents, int $batch } } - if (!$this->getPDO()->commit()) { - throw new Exception('Failed to commit transaction'); - } - - return $documents; - } catch (PDOException $e) { + $this->getPDO()->commit(); + } catch (\Throwable $e) { $this->getPDO()->rollBack(); - throw match ($e->getCode()) { - 1062, 23000 => new DuplicateException('Duplicated document: ' . $e->getMessage()), - default => $e, - }; + if($e instanceof PDOException) { + switch ($e->getCode()) { + case 1062: + case 23000: + throw new DuplicateException('Duplicated document: ' . $e->getMessage()); + } + } + + throw $e; } + + return $documents; } /** @@ -1331,12 +1335,6 @@ public function deleteDocument(string $collection, string $id): bool { $name = $this->filter($collection); - try { - $this->getPDO()->beginTransaction(); - } catch (PDOException $e) { - $this->getPDO()->rollBack(); - } - $sql = " DELETE FROM {$this->getSQLTable($name)} WHERE _uid = :_uid @@ -1359,21 +1357,21 @@ public function deleteDocument(string $collection, string $id): bool $stmtPermissions->bindValue(':_uid', $id); try { + $this->getPDO()->beginTransaction(); + if (!$stmt->execute()) { throw new DatabaseException('Failed to delete document'); } if (!$stmtPermissions->execute()) { throw new DatabaseException('Failed to clean permissions'); } + + $this->getPDO()->commit(); } catch (\Throwable $th) { $this->getPDO()->rollBack(); throw new DatabaseException($th->getMessage()); } - if (!$this->getPDO()->commit()) { - throw new DatabaseException('Failed to commit transaction'); - } - return true; } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 6b9e3795c..d065b9850 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -36,7 +36,11 @@ public function create(string $name): bool { $name = $this->filter($name); - $sql = "CREATE SCHEMA IF NOT EXISTS \"{$name}\""; + if ($this->exists($name)) { + return true; + } + + $sql = "CREATE SCHEMA \"{$name}\""; $sql = $this->trigger(Database::EVENT_DATABASE_CREATE, $sql); return $this->getPDO() diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 39729e8b6..344b74b72 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -50,45 +50,30 @@ public function ping(): bool * @return bool * @throws Exception */ - public function exists(string $database, ?string $collection): bool + public function exists(string $database, ?string $collection = null): bool { $database = $this->filter($database); if (!\is_null($collection)) { $collection = $this->filter($collection); - - $select = 'TABLE_NAME'; - $from = 'INFORMATION_SCHEMA.TABLES'; - $where = 'TABLE_SCHEMA = :schema AND TABLE_NAME = :table'; - $match = "{$this->getNamespace()}_{$collection}"; - } else { - $select = 'SCHEMA_NAME'; - $from = 'INFORMATION_SCHEMA.SCHEMATA'; - $where = 'SCHEMA_NAME = :schema'; - $match = $database; - } - - $stmt = $this->getPDO() - ->prepare("SELECT {$select} - FROM {$from} - WHERE {$where};"); - - $stmt->bindValue(':schema', $database, PDO::PARAM_STR); - - if (!\is_null($collection)) { + $stmt = $this->getPDO()->prepare("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table"); + $stmt->bindValue(':schema', $database, PDO::PARAM_STR); $stmt->bindValue(':table', "{$this->getNamespace()}_{$collection}", PDO::PARAM_STR); + } else { + $stmt = $this->getPDO()->prepare("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :schema"); + $stmt->bindValue(':schema', $database, PDO::PARAM_STR); } $stmt->execute(); $document = $stmt->fetchAll(); $stmt->closeCursor(); - if (!empty($document)) { - $document = $document[0]; + + if (empty($document)) { + return false; } - return (($document[$select] ?? '') === $match) || // case insensitive check - (($document[strtolower($select)] ?? '') === $match); + return true; } /** @@ -844,7 +829,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): { $roles = array_map(fn (string $role) => $this->getPDO()->quote($role), $roles); return "table_main._uid IN ( - SELECT distinct(_document) + SELECT _document FROM {$this->getSQLTable($collection . '_perms')} WHERE _permission IN (" . implode(', ', $roles) . ") AND _type = 'read' diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index c4afe6810..a11c374d5 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -32,12 +32,11 @@ class SQLite extends MariaDB * Optionally check if collection exists in Database * * @param string $database - * @param string $collection + * @param string|null $collection * @return bool - * @throws Exception - * @throws PDOException + * @throws DatabaseException */ - public function exists(string $database, ?string $collection): bool + public function exists(string $database, ?string $collection = null): bool { $database = $this->filter($database); diff --git a/src/Database/Database.php b/src/Database/Database.php index d8393ac8f..a78585bc8 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2667,6 +2667,7 @@ private function populateDocumentRelationships(Document $collection, Document $d * * @param string $collection * @param Document $document + * @param bool $preserveDates If true, createdAt and updatedAt will not be overwritten * * @return Document * @@ -2674,7 +2675,7 @@ private function populateDocumentRelationships(Document $collection, Document $d * @throws DatabaseException * @throws StructureException */ - public function createDocument(string $collection, Document $document): Document + public function createDocument(string $collection, Document $document, bool $preserveDates = false): Document { $collection = $this->silent(fn () => $this->getCollection($collection)); @@ -2687,11 +2688,14 @@ public function createDocument(string $collection, Document $document): Document $time = DateTime::now(); + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$createdAt', $time) - ->setAttribute('$updatedAt', $time); + ->setAttribute('$createdAt', empty($createdAt) || !$preserveDates ? $time : $createdAt) + ->setAttribute('$updatedAt', empty($updatedAt) || !$preserveDates ? $time : $updatedAt); $document = $this->encode($collection, $document); @@ -2728,6 +2732,7 @@ public function createDocument(string $collection, Document $document): Document * @param string $collection * @param array $documents * @param int $batchSize + * @param bool $preserveDates If true, createdAt and updatedAt will not be overwritten * * @return array * @@ -2735,7 +2740,7 @@ public function createDocument(string $collection, Document $document): Document * @throws StructureException * @throws Exception */ - public function createDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE): array + public function createDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE, bool $preserveDates = false): array { if (empty($documents)) { return []; @@ -2746,11 +2751,14 @@ public function createDocuments(string $collection, array $documents, int $batch $time = DateTime::now(); foreach ($documents as $key => $document) { + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$createdAt', $time) - ->setAttribute('$updatedAt', $time); + ->setAttribute('$createdAt', empty($createdAt) || !$preserveDates ? $time : $createdAt) + ->setAttribute('$updatedAt', empty($updatedAt) || !$preserveDates ? $time : $updatedAt); $document = $this->encode($collection, $document); @@ -3055,6 +3063,7 @@ private function relateDocumentsById( * @param string $collection * @param string $id * @param Document $document + * @param bool $preserveDates If true, updatedAt will not be overwritten * @return Document * * @throws AuthorizationException @@ -3062,7 +3071,7 @@ private function relateDocumentsById( * @throws DatabaseException * @throws StructureException */ - public function updateDocument(string $collection, string $id, Document $document): Document + public function updateDocument(string $collection, string $id, Document $document, bool $preserveDates = false): Document { if (!$document->getId() || !$id) { throw new DatabaseException('Must define $id attribute'); @@ -3176,7 +3185,8 @@ public function updateDocument(string $collection, string $id, Document $documen } if ($shouldUpdate) { - $document->setAttribute('$updatedAt', $time); + $updatedAt = $document->getUpdatedAt(); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$preserveDates ? $time : $updatedAt); } // Check if document was updated after the request timestamp @@ -3220,6 +3230,7 @@ public function updateDocument(string $collection, string $id, Document $documen * @param string $collection * @param array $documents * @param int $batchSize + * @param bool $preserveDates If true, updatedAt will not be overwritten * * @return array * @@ -3227,7 +3238,7 @@ public function updateDocument(string $collection, string $id, Document $documen * @throws Exception * @throws StructureException */ - public function updateDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE): array + public function updateDocuments(string $collection, array $documents, int $batchSize = self::INSERT_BATCH_SIZE, bool $preserveDates = false): array { if (empty($documents)) { return []; @@ -3241,7 +3252,8 @@ public function updateDocuments(string $collection, array $documents, int $batch throw new Exception('Must define $id attribute for each document'); } - $document->setAttribute('$updatedAt', $time); + $updatedAt = $document->getUpdatedAt(); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$preserveDates ? $time : $updatedAt); $document = $this->encode($collection, $document); $old = Authorization::skip(fn () => $this->silent( @@ -3834,11 +3846,11 @@ public function deleteDocument(string $collection, string $id): bool $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); } + $deleted = $this->adapter->deleteDocument($collection->getId(), $id); + $this->purgeRelatedDocuments($collection, $id); $this->cache->purge('cache-' . $this->getNamespace() . ':' . $collection->getId() . ':' . $id . ':*'); - $deleted = $this->adapter->deleteDocument($collection->getId(), $id); - $this->trigger(self::EVENT_DOCUMENT_DELETE, $document); return $deleted; @@ -4038,6 +4050,9 @@ private function deleteSetNull(Document $collection, Document $relatedCollection Query::equal($twoWayKey, [$document->getId()]) ]); } else { + if(empty($value)) { + return; + } $related = $this->getDocument($relatedCollection->getId(), $value->getId()); } diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 31b0dc9df..c2727ef59 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -75,6 +75,97 @@ public function testCreateExistsDelete(): void $this->assertEquals(true, static::getDatabase()->create()); } + public function testPreserveDatesUpdate(): void + { + Authorization::disable(); + + static::getDatabase()->createCollection('preserve_update_dates'); + + static::getDatabase()->createAttribute('preserve_update_dates', 'attr1', Database::VAR_STRING, 10, false); + + $doc1 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ + '$id' => 'doc1', + '$permissions' => [], + 'attr1' => 'value1', + ])); + + $doc2 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ + '$id' => 'doc2', + '$permissions' => [], + 'attr1' => 'value2', + ])); + + $doc3 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ + '$id' => 'doc3', + '$permissions' => [], + 'attr1' => 'value3', + ])); + + $newDate = '2000-01-01T10:00:00.000+00:00'; + + $doc1->setAttribute('$updatedAt', $newDate); + static::getDatabase()->updateDocument('preserve_update_dates', 'doc1', $doc1, true); + $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, true); + + $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')); + + static::getDatabase()->deleteCollection('preserve_update_dates'); + + Authorization::reset(); + } + + public function testPreserveDatesCreate(): void + { + Authorization::disable(); + + static::getDatabase()->createCollection('preserve_create_dates'); + + static::getDatabase()->createAttribute('preserve_create_dates', 'attr1', Database::VAR_STRING, 10, false); + + $date = '2000-01-01T10:00:00.000+00:00'; + + static::getDatabase()->createDocument('preserve_create_dates', new Document([ + '$id' => 'doc1', + '$permissions' => [], + 'attr1' => 'value1', + '$createdAt' => $date + ]), true); + + static::getDatabase()->createDocuments('preserve_create_dates', [ + new Document([ + '$id' => 'doc2', + '$permissions' => [], + 'attr1' => 'value2', + '$createdAt' => $date + ]), + new Document([ + '$id' => 'doc3', + '$permissions' => [], + 'attr1' => 'value3', + '$createdAt' => $date + ]), + ], 2, true); + + $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')); + + static::getDatabase()->deleteCollection('preserve_create_dates'); + + Authorization::reset(); + } + /** * @throws Exception|Throwable */ @@ -2116,6 +2207,31 @@ public function testFindByInternalID(array $data): void $this->assertEquals(1, count($documents)); } + /** + * @return void + * @throws \Utopia\Database\Exception + */ + public function testSelectInternalID(): void + { + $documents = static::getDatabase()->find('movies', [ + Query::select(['$internalId', '$id']), + Query::orderAsc(''), + Query::limit(1), + ]); + + $document = $documents[0]; + + $this->assertArrayHasKey('$internalId', $document); + $this->assertCount(2, $document); + + $document = static::getDatabase()->getDocument('movies', $document->getId(), [ + Query::select(['$internalId']), + ]); + + $this->assertArrayHasKey('$internalId', $document); + $this->assertCount(1, $document); + } + public function testFindOrderBy(): void { /** @@ -5571,6 +5687,15 @@ public function testOneToOneTwoWayRelationship(): void onDelete: Database::RELATION_MUTATE_SET_NULL ); + 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(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 static::getDatabase()->deleteDocument('country', 'country1');