Skip to content

Commit c13e77d

Browse files
committed
Two-way relationships: restore partner-side dedup via pair-key set
Source emits both sides of a two-way relationship as separate Column resources, but processing one side already reconciles both physical columns + both Appwrite-level meta docs (utopia's createRelationship/ deleteRelationship cascade). Without dedup, the partner pass can fire DropAndRecreate again — partner's source createdAt is independent of parent's and may not match what we just stamped on dest's partner meta (parent's createdAt). Reintroduces the pair-key dedup that existed in 791dbb1 and was removed in 6e6f825 when relationships routed exclusively through UpdateInPlace. Commit 786a27b reopened the DropAndRecreate path for createdAt-different without restoring the dedup; this fixes that gap. Pair-key is canonical (sorted, scoped to dbSeq) so both sides resolve to the same string. Set after Tolerate, after UpdateInPlace success, and after the create-path completion. Skip check at the top of createField.
1 parent c76de9a commit c13e77d

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

src/Migration/Destinations/Appwrite.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ class Appwrite extends Destination
119119
*/
120120
private array $orphansByTable = [];
121121

122+
/**
123+
* Two-way relationship pairs already reconciled this run, keyed by a
124+
* canonical pair-key. Source emits both sides as separate Column
125+
* resources, but processing one side already reconciles both physical
126+
* columns + both Appwrite-level meta docs. Without this set, the
127+
* partner pass can re-trigger DropAndRecreate and destroy partner-table
128+
* rows already migrated.
129+
*
130+
* @var array<string, true>
131+
*/
132+
private array $processedTwoWayPairs = [];
133+
122134
/**
123135
* @param string $project
124136
* @param string $endpoint
@@ -775,6 +787,17 @@ protected function createField(Column|Attribute $resource): bool
775787
// Two-way drop loses partner row data, restored by the subsequent Upsert row pass.
776788
$isRelationship = $type === UtopiaDatabase::VAR_RELATIONSHIP;
777789

790+
// Two-way relationships emit two source-side resources but reconcile both
791+
// physical columns + both Appwrite-level meta docs in one pass. Skip the
792+
// partner — re-processing can fire DropAndRecreate again and destroy
793+
// partner-table rows already migrated this run (parent and partner have
794+
// independent source createdAt values that may not match).
795+
$twoWayPairKey = $this->twoWayPairKey($database, $table, $resource, $type);
796+
if ($twoWayPairKey !== null && isset($this->processedTwoWayPairs[$twoWayPairKey])) {
797+
$resource->setStatus(Resource::STATUS_SKIPPED, 'Two-way partner already reconciled');
798+
return false;
799+
}
800+
778801
$attributeMetaId = $this->attributeIndexMetaId($database, $table, $resource->getKey());
779802
if ($this->onDuplicate !== OnDuplicate::Fail) {
780803
$existingAttr = $this->dbForProject->getDocument('attributes', $attributeMetaId);
@@ -802,6 +825,9 @@ protected function createField(Column|Attribute $resource): bool
802825
SchemaAction::DropAndRecreate, SchemaAction::Create => null,
803826
};
804827
if ($earlyReturn !== null) {
828+
if ($twoWayPairKey !== null) {
829+
$this->processedTwoWayPairs[$twoWayPairKey] = true;
830+
}
805831
return $earlyReturn;
806832
}
807833

@@ -972,6 +998,10 @@ protected function createField(Column|Attribute $resource): bool
972998

973999
$this->purgeTableCaches($database, $table, $dbForDatabases);
9741000

1001+
if ($twoWayPairKey !== null) {
1002+
$this->processedTwoWayPairs[$twoWayPairKey] = true;
1003+
}
1004+
9751005
return true;
9761006
}
9771007

@@ -1510,6 +1540,37 @@ private function tableIdentity(UtopiaDocument $database, UtopiaDocument $table):
15101540
return $database->getSequence() . ':' . $table->getSequence();
15111541
}
15121542

1543+
/**
1544+
* Canonical pair-key for a two-way relationship — same string regardless
1545+
* of which side (parent or partner) is being processed. Returns null for
1546+
* non-two-way attributes (no pair-level tracking needed).
1547+
*/
1548+
private function twoWayPairKey(
1549+
UtopiaDocument $database,
1550+
UtopiaDocument $table,
1551+
Column|Attribute $resource,
1552+
string $type,
1553+
): ?string {
1554+
if ($type !== UtopiaDatabase::VAR_RELATIONSHIP) {
1555+
return null;
1556+
}
1557+
$options = $resource->getOptions();
1558+
if (empty($options['twoWay'])) {
1559+
return null;
1560+
}
1561+
$twoWayKey = (string) ($options['twoWayKey'] ?? '');
1562+
$relatedTableId = (string) ($options['relatedCollection'] ?? '');
1563+
if ($twoWayKey === '' || $relatedTableId === '') {
1564+
return null;
1565+
}
1566+
1567+
$thisSide = $table->getId() . '::' . $resource->getKey();
1568+
$partnerSide = $relatedTableId . '::' . $twoWayKey;
1569+
$pair = [$thisSide, $partnerSide];
1570+
\sort($pair);
1571+
return $database->getSequence() . '@' . \implode('<->', $pair);
1572+
}
1573+
15131574
private function databaseCollectionId(UtopiaDocument $database): string
15141575
{
15151576
return 'database_' . $database->getSequence();

0 commit comments

Comments
 (0)