From 17c1e8d570e28418cf0495b03ee7bad411907b54 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 1 May 2026 14:31:56 +1200 Subject: [PATCH] fix(migrations): resolveResourceIds preserves single-root semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloud's backup migrations write resourceId=databaseId, resourceType="database" — a single root resource with no child filter. The previous resolveResourceIds() unconditionally treated resourceId as the leaf (collection/table), so for backup docs it returned ["", databaseId] and Transfer::run was invoked with rootResourceId="", rootResourceChildId=databaseId. Sources/Appwrite then tried to filter tables by id=databaseId and matched zero, producing empty backups and restore failures. Resolve to [rootResourceId, rootResourceChildId] now: - parentResourceId set → [parent, leaf] (new-shape CSV/JSON) - resourceId is "x:y" → split (legacy CSV/JSON) - otherwise → [resourceId, ''] (backup, full imports) CSV/JSON paths still see [database, collection]. Backup paths now correctly see [database, ''] so the upstream lib filters to just that database with no leaf scope. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Appwrite/Platform/Workers/Migrations.php | 36 ++++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 5b22d73478..ba8d01942a 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -615,29 +615,37 @@ class Migrations extends Action } /** - * Resolve [databaseId, tableId] from a migration document. + * Resolve [rootResourceId, rootResourceChildId] from a migration document. * - * Handles three input shapes so a 1.9.4 worker keeps working before V25 - * has run, while in-flight migrations enqueued on 1.9.3 drain, and after - * a backup taken on 1.9.3 is restored: - * - 1.9.4 (post-backfill): parentResourceId/resourceId set separately. - * - 1.9.3 (legacy): resourceId is the composite "{databaseId}:{tableId}", - * parentResourceId is empty. - * - 1.9.4 mid-backfill: V25 attributes added but a given doc not yet - * rewritten, same as the legacy case. + * Returns the root resource ID (e.g. databaseId for CSV/JSON or backup + * migrations) and an optional child filter under that root (the + * collection/table ID for CSV/JSON migrations). Three input shapes: + * + * - 1.9.4 CSV/JSON migration: parentResourceId is set to the root, + * resourceId is the leaf. Returns [parent, leaf]. + * - Pre-1.9.4 CSV/JSON migration (V25 not yet run, queue drain after + * upgrade, restored backup): resourceId is the composite + * "{databaseId}:{tableId}", parentResourceId is empty. Split it. + * - Single-root migration (Backup, full Appwrite/Supabase/Firebase/ + * NHost imports): resourceId is the root resource ID with no leaf. + * Returns [root, '']. * * @return array{0: string, 1: string} */ protected function resolveResourceIds(Document $migration): array { - $databaseId = (string) $migration->getAttribute('parentResourceId', ''); - $tableId = (string) $migration->getAttribute('resourceId', ''); + $parent = (string) $migration->getAttribute('parentResourceId', ''); + $resource = (string) $migration->getAttribute('resourceId', ''); - if ($databaseId === '' && \str_contains($tableId, ':')) { - [$databaseId, $tableId] = \explode(':', $tableId, 2); + if ($parent !== '') { + return [$parent, $resource]; } - return [$databaseId, $tableId]; + if (\str_contains($resource, ':')) { + return \explode(':', $resource, 2); + } + + return [$resource, '']; } /**