diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 7b86d6d00..dfec68965 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -472,6 +472,44 @@ public function updateAttribute(string $collection, string $key, Attribute $attr } } + /** + * Dropping a column drops an index only when no other column is left in it. Under shared + * tables every key and unique index leads with the tenant column, so an index whose last + * attribute is the dropped column survives on the tenant column alone, keeps its name, and + * refuses the next index created under that name. Every index left on the tenant column + * alone is dropped with the column: no index the library creates is ever only that column. + * + * @throws DatabaseException + * @throws Exception + * @throws PDOException + */ + #[\Override] + public function deleteAttribute(string $collection, string $key): bool + { + $missing = null; + try { + $deleted = parent::deleteAttribute($collection, $key); + } catch (NotFoundException $error) { + // A retry after the column was dropped still clears what the first attempt left behind. + $missing = $error; + $deleted = false; + } + + if ($this->sharedTables) { + foreach ($this->getSchemaIndexes($collection) as $index) { + if (\array_map(\strtolower(...), $index->columns) === [\strtolower(Storage::TENANT)]) { + $this->deleteIndex($collection, $index->name); + } + } + } + + if ($missing !== null) { + throw $missing; + } + + return $deleted; + } + /** * @param array $indexAttributeTypes * @param array $collation diff --git a/tests/e2e/Adapter/Scopes/SchemaReconciliationTests.php b/tests/e2e/Adapter/Scopes/SchemaReconciliationTests.php index e497aedf4..87c22bd43 100644 --- a/tests/e2e/Adapter/Scopes/SchemaReconciliationTests.php +++ b/tests/e2e/Adapter/Scopes/SchemaReconciliationTests.php @@ -4,6 +4,8 @@ use Throwable; use Utopia\Database\Adapter\Feature; +use Utopia\Database\Adapter\Mongo; +use Utopia\Database\Adapter\Postgres; use Utopia\Database\Attribute; use Utopia\Database\Capability; use Utopia\Database\Collection; @@ -230,6 +232,43 @@ public function testAnOrphanIndexOfTheRequestedDefinitionIsReused(): void } } + public function testDeletingAnAttributeDropsItsOnlyIndexFromTheSchema(): void + { + $database = $this->getDatabase(); + $adapter = $database->getAdapter(); + + // Mongo unsets the field and keeps its indexes. + if (! $adapter->supports(Capability::SchemaIntrospection) || $adapter instanceof Mongo) { + $this->expectNotToPerformAssertions(); + + return; + } + + $collection = 'reconcileDroppedAttributeIndex'; + $this->createReconciledCollection($database, $collection, [ + Attribute::string(key: 'status', size: 255), + Attribute::string(key: 'name', size: 255), + ]); + + try { + $database->createIndex($collection, Index::key(key: 'status_index', attributes: ['status'])); + $database->createIndex($collection, Index::key(key: 'status_name', attributes: ['status', 'name'])); + + $database->deleteAttribute($collection, 'status'); + + $this->assertFalse($this->hasSchemaIndex($database, $collection, 'status_index')); + // Postgres drops every index on a dropped column, composite ones included. + if (! $adapter instanceof Postgres) { + $this->assertTrue($this->hasSchemaIndex($database, $collection, 'status_name')); + } + + $database->createAttribute($collection, Attribute::string(key: 'status', size: 255)); + $this->assertSame('status_index', $database->createIndex($collection, Index::key(key: 'status_index', attributes: ['status']))->key); + } finally { + $database->deleteCollection($collection); + } + } + /** * @param list $attributes */