diff --git a/src/Database/Trait/Attributes.php b/src/Database/Trait/Attributes.php index cf5cd6d2a..1300dd4a3 100644 --- a/src/Database/Trait/Attributes.php +++ b/src/Database/Trait/Attributes.php @@ -378,6 +378,17 @@ public function deleteAttribute(string $collection, string $key): void $deletedInSchema = $this->deleteAttributeFromSchema($definition->getId(), $key); + // An engine drops an index with its column only when no other column is left in it, and under + // shared tables every index also holds the tenant column. An index whose last attribute this was + // would survive on the tenant column alone and refuse the next index created under its name. + if ($this->adapter->hasSharedTables()) { + foreach ($indexes as $index) { + if ($index->attributes === [$key]) { + $this->deleteIndexFromSchema($definition->getId(), $index->key); + } + } + } + $this->updateMetadata( collection: $definition, rollbackOperation: fn () => $this->adapter->createAttribute($definition->getId(), $attribute), 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 */