Skip to content

Commit 4e8100a

Browse files
committed
Upsert drop: clean up child-side metadata for two-way relationships
On a first-run migration createField writes TWO metadata documents for a two-way relationship attribute: parent-side at {dbSeq}_{tableSeq}_{key} (line 763) and child-side at {dbSeq}_{relatedTableSeq}_{twoWayKey} (line 819). Migration operates directly on the database via $dbForProject — it doesn't rely on Appwrite's attribute-event workers to derive the child side — so both documents are physically written by migration itself. The Upsert drop block was only removing the parent-side document + column. On the recreate pass that followed, line 819 re-wrote the child-side document and immediately hit DuplicateException because the stale child doc from the previous migration run was still sitting there. The inner catch rolled back the parent and the whole attribute aborted with "Attribute already exists" — breaking Upsert re-migration for every two-way relationship whose source spec had changed. Fix: in the Upsert drop branch, when the resource is a two-way relationship, also delete the child-side metadata document and the child-side physical column on the related table, then purge caches. Both deletes are wrapped in try/catch to tolerate the case where a prior interrupted run (or utopia-php/database's relationship handling) already cleaned one side — the goal is to guarantee a clean slate for the downstream recreate, not to require both sides to still exist. Flagged by greptile P1. Reproducing scenario: - OnDuplicate::Upsert - Destination already contains the attribute - Source's two-way relationship spec was modified between runs (source.updatedAt > dest.updatedAt)
1 parent fc6e5f7 commit 4e8100a

1 file changed

Lines changed: 31 additions & 0 deletions

File tree

src/Migration/Destinations/Appwrite.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,37 @@ protected function createField(Column|Attribute $resource): bool
732732
$this->dbForProject->deleteDocument('attributes', $attributeMetaId);
733733
$this->dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $table->getId());
734734
$dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $table->getSequence());
735+
736+
// Two-way relationships: the first-run createField wrote TWO
737+
// metadata docs (parent at line 763, child at line 819). The
738+
// drop above only removed the parent; without cleaning up the
739+
// child, the recreate path hits DuplicateException at line 819
740+
// and the whole attribute re-migration fails.
741+
$resourceOptions = $resource->getOptions();
742+
if ($type === UtopiaDatabase::VAR_RELATIONSHIP && !empty($resourceOptions['twoWay'])) {
743+
// $relatedTable was populated at the top of createField
744+
// inside the same VAR_RELATIONSHIP branch.
745+
$childTwoWayKey = $resourceOptions['twoWayKey'] ?? '';
746+
if ($childTwoWayKey !== '') {
747+
$childMetaId = $database->getSequence() . '_' . $relatedTable->getSequence() . '_' . $childTwoWayKey;
748+
try {
749+
$this->dbForProject->deleteDocument('attributes', $childMetaId);
750+
} catch (\Throwable) {
751+
// Child metadata already gone — interrupted prior run, nothing to do.
752+
}
753+
try {
754+
$dbForDatabases->deleteAttribute(
755+
'database_' . $database->getSequence() . '_collection_' . $relatedTable->getSequence(),
756+
$childTwoWayKey
757+
);
758+
} catch (\Throwable) {
759+
// Child column already gone — the parent's deleteAttribute may
760+
// have cascaded via utopia-php/database's relationship handling.
761+
}
762+
$this->dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $relatedTable->getId());
763+
$dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $relatedTable->getSequence());
764+
}
765+
}
735766
}
736767
}
737768

0 commit comments

Comments
 (0)