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..868a0918d 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -438,7 +438,7 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa return true; } - throw $e; + throw $this->processException($e); } } @@ -462,9 +462,13 @@ 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 +770,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 +802,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 6256c958a..6aba14d0b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2058,34 +2058,40 @@ 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); + $indexAttributes = \array_map(fn ($attr) => ($attr === $old) ? $new : $attr, $indexAttributes); $index->setAttribute('attributes', $indexAttributes); } @@ -2099,7 +2105,7 @@ public function renameAttribute(string $collection, string $old, string $new): b $this->silent(fn () => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $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..5c58ef63c --- /dev/null +++ b/src/Database/Exception/Dependency.php @@ -0,0 +1,9 @@ +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,32 @@ 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'], [100]); + + 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->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_new')); + } + try { $database->createIndex($collection, 'indx', Database::INDEX_FULLTEXT, ['names']); $this->fail('Failed to throw exception');