diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index ec9bd9001..1a536bfce 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -708,9 +708,14 @@ 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); - return $this->getPDO() - ->prepare($sql) - ->execute(); + try { + return $this->getPDO() + ->prepare($sql) + ->execute(); + } catch (PDOException $e) { + $this->processException($e); + return false; + } } /** @@ -1651,6 +1656,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $where = []; $orders = []; + $queries = array_map(fn ($query) => clone $query, $queries); + $orderAttributes = \array_map(fn ($orderAttribute) => match ($orderAttribute) { '$id' => '_uid', '$internalId' => '_id', @@ -1852,6 +1859,8 @@ public function count(string $collection, array $queries = [], ?int $max = null) $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; + $queries = array_map(fn ($query) => clone $query, $queries); + $conditions = $this->getSQLConditions($queries); if(!empty($conditions)) { $where[] = $conditions; @@ -1923,6 +1932,8 @@ public function sum(string $collection, string $attribute, array $queries = [], $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; + $queries = array_map(fn ($query) => clone $query, $queries); + foreach ($queries as $query) { $where[] = $this->getSQLCondition($query); } @@ -2242,6 +2253,13 @@ protected function processException(PDOException $e): void throw 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) { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 1061 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42000') { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } + throw $e; } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 1b0ce8e6f..feb1d9a15 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -951,6 +951,7 @@ public function updateAttribute(string $collection, string $id, string $type, in 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 { $name = $this->getNamespace() . '_' . $this->filter($collection); + $queries = array_map(fn ($query) => clone $query, $queries); $filters = $this->buildFilters($queries); @@ -1212,6 +1213,8 @@ public function count(string $collection, array $queries = [], ?int $max = null) { $name = $this->getNamespace() . '_' . $this->filter($collection); + $queries = array_map(fn ($query) => clone $query, $queries); + $filters = []; $options = []; @@ -1252,6 +1255,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $name = $this->getNamespace() . '_' . $this->filter($collection); // queries + $queries = array_map(fn ($query) => clone $query, $queries); $filters = $this->buildFilters($queries); // permissions diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 0183a93c8..fb0005f23 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -35,37 +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 - { - /** - * PDO and Swoole PDOProxy swap error codes and errorInfo - */ - - 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() === '42S21' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1060) { - throw new DuplicateException($e->getMessage(), $e->getCode(), $e); - } elseif ($e->getCode() === 1060 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S21') { - throw 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) { - throw new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); - } - - throw $e; - } - /** * Get Collection Size * @param string $collection @@ -113,4 +82,44 @@ public function castIndexArray(): bool { return true; } + + /** + * @param PDOException $e + * @throws TimeoutException + * @throws DuplicateException + */ + protected function processException(PDOException $e): void + { + /** + * PDO and Swoole PDOProxy swap error codes and errorInfo + */ + + // Timeout + 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); + } + + // Duplicate column + if ($e->getCode() === '42S21' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1060) { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 1060 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42S21') { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } + + // Duplicate index + if ($e->getCode() === '42000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1061) { + throw new DuplicateException($e->getMessage(), $e->getCode(), $e); + } elseif ($e->getCode() === 1061 && isset($e->errorInfo[0]) && $e->errorInfo[0] === '42000') { + throw 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) { + throw new DatabaseException('Resize would result in data truncation', $e->getCode(), $e); + } + + throw $e; + } } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index fc49e1486..dee2a8614 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -686,9 +686,14 @@ public function createIndex(string $collection, string $id, string $type, array $sql = "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)} ({$attributes});"; $sql = $this->trigger(Database::EVENT_INDEX_CREATE, $sql); - return $this->getPDO() - ->prepare($sql) - ->execute(); + try { + return $this->getPDO() + ->prepare($sql) + ->execute(); + } catch (PDOException $e) { + $this->processException($e); + return false; + } } /** @@ -1578,6 +1583,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $where = []; $orders = []; + $queries = array_map(fn ($query) => clone $query, $queries); + $orderAttributes = \array_map(fn ($orderAttribute) => match ($orderAttribute) { '$id' => '_uid', '$internalId' => '_id', @@ -1773,6 +1780,8 @@ public function count(string $collection, array $queries = [], ?int $max = null) $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; + $queries = array_map(fn ($query) => clone $query, $queries); + $conditions = $this->getSQLConditions($queries); if(!empty($conditions)) { $where[] = $conditions; @@ -1837,6 +1846,8 @@ public function sum(string $collection, string $attribute, array $queries = [], $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; + $queries = array_map(fn ($query) => clone $query, $queries); + foreach ($queries as $query) { $where[] = $this->getSQLCondition($query); } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 422d6c5d5..93d3c574f 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1367,12 +1367,14 @@ protected function processException(PDOException $e): void * PDO and Swoole PDOProxy swap error codes and errorInfo */ + // Timeout 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); } + // Duplicate if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1) { throw new DuplicateException($e->getMessage(), $e->getCode(), $e); } elseif ($e->getCode() === 1 && isset($e->errorInfo[0]) && $e->errorInfo[0] === 'HY000') { diff --git a/src/Database/Database.php b/src/Database/Database.php index cf5d52020..f52ba17c9 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2660,7 +2660,18 @@ public function createIndex(string $collection, string $id, string $type, array } } - $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders); + try { + $created = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders); + + if (!$created) { + throw new DatabaseException('Failed to create index'); + } + } catch (DuplicateException $e) { + // HACK: Metadata should still be updated, can be removed when null tenant collections are supported. + if (!$this->adapter->getSharedTables()) { + throw $e; + } + } if ($collection->getId() !== self::METADATA) { $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); @@ -2668,7 +2679,7 @@ public function createIndex(string $collection, string $id, string $type, array $this->trigger(self::EVENT_INDEX_CREATE, $index); - return $index; + return true; } /** diff --git a/src/Database/Query.php b/src/Database/Query.php index b01534aab..6af553415 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -91,6 +91,15 @@ public function __construct(string $method, string $attribute = '', array $value $this->values = $values; } + public function __clone(): void + { + foreach ($this->values as $index => $value) { + if ($value instanceof self) { + $this->values[$index] = clone $value; + } + } + } + /** * @return string */ diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 263215d46..97cbf40e7 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -1221,7 +1221,7 @@ public function testCreateDeleteAttribute(): void $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 shared tables duplicates throw duplicate + // Test non-shared tables duplicates throw duplicate static::getDatabase()->createAttribute('attributes', 'duplicate', Database::VAR_STRING, 128, true); try { static::getDatabase()->createAttribute('attributes', 'duplicate', Database::VAR_STRING, 128, true); @@ -1270,6 +1270,7 @@ 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, static::getDatabase()->createAttribute('attributes', 'CaseSensitive', Database::VAR_STRING, 128, true)); @@ -1428,6 +1429,15 @@ public function testCreateDeleteIndex(): void $collection = static::getDatabase()->getCollection('indexes'); $this->assertCount(0, $collection->getAttribute('indexes')); + // Test non-shared tables duplicates throw duplicate + static::getDatabase()->createIndex('indexes', 'duplicate', Database::INDEX_KEY, ['string', 'boolean'], [128], [Database::ORDER_ASC]); + try { + static::getDatabase()->createIndex('indexes', 'duplicate', Database::INDEX_KEY, ['string', 'boolean'], [128], [Database::ORDER_ASC]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(DuplicateException::class, $e); + } + static::getDatabase()->deleteCollection('indexes'); } @@ -4142,6 +4152,67 @@ public function testAndNested(): void $this->assertEquals(3, $count); } + public function testNestedIDQueries(): void + { + Authorization::setRole(Role::any()->toString()); + + static::getDatabase()->createCollection('movies_nested_id', permissions: [ + Permission::create(Role::any()), + Permission::update(Role::users()) + ]); + + $this->assertEquals(true, static::getDatabase()->createAttribute('movies_nested_id', 'name', Database::VAR_STRING, 128, true)); + + static::getDatabase()->createDocument('movies_nested_id', new Document([ + '$id' => ID::custom('1'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => '1', + ])); + + static::getDatabase()->createDocument('movies_nested_id', new Document([ + '$id' => ID::custom('2'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => '2', + ])); + + static::getDatabase()->createDocument('movies_nested_id', new Document([ + '$id' => ID::custom('3'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'name' => '3', + ])); + + $queries = [ + Query::or([ + Query::equal('$id', ["1"]), + Query::equal('$id', ["2"]) + ]) + ]; + + $documents = static::getDatabase()->find('movies_nested_id', $queries); + $this->assertCount(2, $documents); + + // Make sure the query was not modified by reference + $this->assertEquals($queries[0]->getValues()[0]->getAttribute(), '$id'); + + $count = static::getDatabase()->count('movies_nested_id', $queries); + $this->assertEquals(2, $count); + } + /** * @depends testFind */ @@ -15360,7 +15431,7 @@ public function testSharedTables(): void $database->setDatabase($this->testDatabase); } - public function testSharedTablesDuplicateAttributesDontThrow(): void + public function testSharedTablesDuplicatesDontThrow(): void { $database = static::getDatabase(); @@ -15383,22 +15454,28 @@ public function testSharedTablesDuplicateAttributesDontThrow(): void // Create collection $database->createCollection('duplicates', documentSecurity: false); $database->createAttribute('duplicates', 'name', Database::VAR_STRING, 10, false); + $database->createIndex('duplicates', 'nameIndex', Database::INDEX_KEY, ['name']); $database->setTenant(2); + try { $database->createCollection('duplicates', documentSecurity: false); } catch (DuplicateException) { // Ignore } + $database->createAttribute('duplicates', 'name', Database::VAR_STRING, 10, false); + $database->createIndex('duplicates', 'nameIndex', Database::INDEX_KEY, ['name']); $collection = $database->getCollection('duplicates'); $this->assertEquals(1, \count($collection->getAttribute('attributes'))); + $this->assertEquals(1, \count($collection->getAttribute('indexes'))); $database->setTenant(1); $collection = $database->getCollection('duplicates'); $this->assertEquals(1, \count($collection->getAttribute('attributes'))); + $this->assertEquals(1, \count($collection->getAttribute('indexes'))); $database->setSharedTables(false); $database->setNamespace(static::$namespace);