-
Notifications
You must be signed in to change notification settings - Fork 5
updated db creation flow #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
95e8e9d
15bf41d
4a542d3
fc8b7cd
6488342
88a0429
c6c928f
c48c13e
59d6e32
74be0aa
3d8a201
8dcef54
dbfdb18
758f7da
6b140e8
9003dea
8db38d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,11 @@ class Appwrite extends Destination | |
| private const META_ATTRIBUTES = 'attributes'; | ||
| private const META_INDEXES = 'indexes'; | ||
|
|
||
| /** A database is provisioning while its resources transfer, ready once the run completes, or failed if creation errored. */ | ||
| private const DATABASE_STATUS_PROVISIONING = 'provisioning'; | ||
| private const DATABASE_STATUS_READY = 'ready'; | ||
| private const DATABASE_STATUS_FAILED = 'failed'; | ||
|
|
||
| /** Attribute fields the SDK can't update in place (no per-type updateX endpoint exposes them); a change here forces drop+recreate. */ | ||
| private const ATTRIBUTE_IMMUTABLE_FIELDS = [ | ||
| 'type', | ||
|
|
@@ -164,6 +169,14 @@ class Appwrite extends Destination | |
| */ | ||
| private array $processedTwoWayPairs = []; | ||
|
|
||
| /** | ||
| * Databases created this run, held in `provisioning` until the run finishes | ||
| * successfully. The end-of-run sweep flips each to `ready`. Keyed by database id. | ||
| * | ||
| * @var array<string, true> | ||
| */ | ||
| private array $provisioningDatabases = []; | ||
|
|
||
| /** | ||
| * @param string $project | ||
| * @param string $endpoint | ||
|
|
@@ -223,6 +236,18 @@ private function resolveDestinationDsn(Database $resource): string | |
| return ($this->getDatabaseDSN)($resource); | ||
| } | ||
|
|
||
| private function getSupportForDatabaseStatus(): bool | ||
| { | ||
| // $source is a non-nullable typed property with no default; it is only set when the | ||
| // destination runs through Transfer. Guard so direct createDatabase() calls (e.g. tests) | ||
| // don't hit "must not be accessed before initialization". | ||
| if (! isset($this->source)) { | ||
| return false; | ||
| } | ||
|
|
||
| return $this->getSource()->supportsDatabaseStatus(); | ||
| } | ||
|
|
||
| /** Orphan cleanup runs only after a successful migration — a mid-run throw preserves the destination as-is. */ | ||
| #[Override] | ||
| public function run( | ||
|
|
@@ -233,6 +258,9 @@ public function run( | |
| ): void { | ||
| $this->resetRunState(); | ||
| parent::run($resources, $callback, $rootResourceId, $rootResourceType); | ||
| // parent::run() returning means every resource transferred, so the databases are usable. | ||
| // Flip status before the orphan sweep so a cleanup failure can't strand them in `provisioning`. | ||
| $this->markProvisionedDatabasesReady(); | ||
| $this->cleanupOverwriteOrphans(); | ||
| } | ||
|
|
||
|
|
@@ -242,6 +270,41 @@ private function resetRunState(): void | |
| $this->rowBuffer = []; | ||
| $this->orphansByTable = []; | ||
| $this->processedTwoWayPairs = []; | ||
| $this->provisioningDatabases = []; | ||
| } | ||
|
|
||
| private function markProvisionedDatabasesReady(): void | ||
| { | ||
| foreach (\array_keys($this->provisioningDatabases) as $databaseId) { | ||
| try { | ||
| $this->setDatabaseStatus($databaseId, self::DATABASE_STATUS_READY); | ||
| } catch (\Throwable) { | ||
| // Best-effort: a transient error on one database must not strand the rest | ||
| // in provisioning or block the orphan-cleanup sweep that follows. | ||
| } | ||
| } | ||
| } | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
|
|
||
| private function setDatabaseStatus(string $databaseId, string $status): void | ||
| { | ||
| if (! $this->getSupportForDatabaseStatus()) { | ||
| return; | ||
| } | ||
|
|
||
| $this->dbForProject->updateDocument( | ||
| self::META_DATABASES, | ||
| $databaseId, | ||
| new UtopiaDocument(['status' => $status]), | ||
| ); | ||
| } | ||
|
|
||
| /** Best-effort transition to `failed`; a secondary error here must not mask the caller's original throw. */ | ||
| private function markDatabaseFailed(string $databaseId): void | ||
| { | ||
| try { | ||
| $this->setDatabaseStatus($databaseId, self::DATABASE_STATUS_FAILED); | ||
| } catch (\Throwable) { | ||
| } | ||
| } | ||
|
|
||
| public static function getName(): string | ||
|
|
@@ -508,13 +571,21 @@ protected function import(array $resources, callable $callback): void | |
| */ | ||
| public function importDatabaseResource(Resource $resource, bool $isLast): Resource | ||
| { | ||
| $success = false; | ||
|
|
||
| switch ($resource->getName()) { | ||
| case Resource::TYPE_DATABASE: | ||
| case Resource::TYPE_DATABASE_DOCUMENTSDB: | ||
| case Resource::TYPE_DATABASE_VECTORSDB: | ||
| /** @var Database $resource */ | ||
| $success = $this->createDatabase($resource); | ||
| break; | ||
| try { | ||
| $this->dbForProject->setPreserveDates(false); | ||
| /** @var Database $resource */ | ||
| $success = $this->createDatabase($resource); | ||
| break; | ||
| } finally { | ||
| $this->dbForProject->setPreserveDates(true); | ||
| } | ||
|
greptile-apps[bot] marked this conversation as resolved.
Comment on lines
+580
to
+587
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Before this PR, the outer resource-processing loop set Prompt To Fix With AIThis is a comment left during a code review.
Path: src/Migration/Destinations/Appwrite.php
Line: 580-587
Comment:
**`setPreserveDates(false)` silently discards source timestamps for the entire database creation path**
Before this PR, the outer resource-processing loop set `setPreserveDates(true)` before calling `importDatabaseResource`, so `createDatabase` ran with date preservation active and the explicitly computed `$createdAt`/`$updatedAt` (lines 644–645, 744–747) were written to the destination. This PR wraps the database case in `setPreserveDates(false)`, which means the `$createdAt`/`$updatedAt` fields in the `createDocument` call — and the `$updatedAt` in the Overwrite path's `updateDocument` — are all overridden with the current wall-clock time. Migrated databases will show the migration run time instead of the original source creation/update time, silently breaking the timestamp-preservation contract that every other resource type still honours. If the intent is only to suppress date preservation for the `createCollection` system-collection call, that call should be wrapped independently rather than wrapping all of `createDatabase`.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its intentional for the database
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it — makes sense that database-level timestamp handling differs from other resources. Thanks for the clarification! |
||
|
|
||
| case Resource::TYPE_TABLE: | ||
| case Resource::TYPE_COLLECTION: | ||
| /** @var Table $resource */ | ||
|
|
@@ -580,28 +651,84 @@ protected function createDatabase(Database $resource): bool | |
| $updatedAt, | ||
| $existing->getUpdatedAt(), | ||
| ); | ||
| // Spec match → skip work. Create excluded; nothing on dest to match against. | ||
| if ($action !== SchemaAction::Create && $this->databaseSpecMatches($existing, $resource)) { | ||
|
|
||
| $isFailed = ! $existing->isEmpty() | ||
| && $this->getSupportForDatabaseStatus() | ||
| && $existing->getAttribute('status') === self::DATABASE_STATUS_FAILED; | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
|
|
||
| if ($isFailed) { | ||
| // A prior run created the metadata document but left the database unusable (its backing | ||
| // collection may be missing). Force Overwrite — regardless of timestamps or spec match — | ||
| // so the recovery path recreates the collection instead of skipping it forever. | ||
| $action = SchemaAction::Overwrite; | ||
| } elseif ($action !== SchemaAction::Create && $this->databaseSpecMatches($existing, $resource)) { | ||
| // Spec match → skip work. Create excluded; nothing on dest to match against. | ||
| $action = SchemaAction::Skip; | ||
| } | ||
|
|
||
| $earlyReturn = match ($action) { | ||
| SchemaAction::Skip => (function () use ($resource, $existing): bool { | ||
| $resource->setSequence($existing->getSequence()); | ||
| $resource->setStatus(Resource::STATUS_SKIPPED, 'Already exists on destination'); | ||
| // Recover a database left in `provisioning` by a prior failed run: the spec matches so | ||
| // we skip re-import, but the end-of-run sweep should still flip it to `ready`. | ||
| if ( | ||
| $this->getSupportForDatabaseStatus() | ||
| && $existing->getAttribute('status') === self::DATABASE_STATUS_PROVISIONING | ||
| ) { | ||
| $this->provisioningDatabases[$resource->getId()] = true; | ||
| } | ||
| return false; | ||
| })(), | ||
| SchemaAction::Overwrite => (function () use ($resource, $existing, $updatedAt): bool { | ||
| $this->dbForProject->updateDocument(self::META_DATABASES, $existing->getId(), new UtopiaDocument([ | ||
| SchemaAction::Overwrite => (function () use ($resource, $existing, $updatedAt, $isFailed): bool { | ||
| $document = [ | ||
| 'name' => $resource->getDatabaseName(), | ||
| 'search' => implode(' ', [$resource->getId(), $resource->getDatabaseName()]), | ||
| 'enabled' => $resource->getEnabled(), | ||
| 'type' => empty($resource->getType()) ? 'legacy' : $resource->getType(), | ||
| 'originalId' => empty($resource->getOriginalId()) ? null : $resource->getOriginalId(), | ||
| 'database' => $this->resolveDestinationDsn($resource), | ||
| '$updatedAt' => $updatedAt, | ||
| ])); | ||
| ]; | ||
|
|
||
| if ($this->getSupportForDatabaseStatus()) { | ||
| $document['status'] = self::DATABASE_STATUS_PROVISIONING; | ||
| } | ||
|
|
||
| $this->dbForProject->updateDocument(self::META_DATABASES, $existing->getId(), new UtopiaDocument($document)); | ||
| $resource->setSequence($existing->getSequence()); | ||
|
|
||
| // Only a `failed` database can be missing its backing collection (a prior run wrote the | ||
| // metadata document but threw before createCollection). Recreate it so we never flip a | ||
| // database to ready with no collection behind it. A healthy overwrite already has its | ||
| // collection, so we skip the lookup entirely. | ||
| if ($isFailed && $this->dbForProject->getCollection($this->databaseCollectionId($existing))->isEmpty()) { | ||
| try { | ||
| $columns = \array_map( | ||
| fn ($attr) => new UtopiaDocument($attr), | ||
| $this->collectionStructure['attributes'] | ||
| ); | ||
|
|
||
| $indexes = \array_map( | ||
| fn ($index) => new UtopiaDocument($index), | ||
| $this->collectionStructure['indexes'] | ||
| ); | ||
|
|
||
| $this->dbForProject->createCollection( | ||
| $this->databaseCollectionId($existing), | ||
| $columns, | ||
| $indexes | ||
| ); | ||
| } catch (\Throwable $e) { | ||
| $this->markDatabaseFailed($resource->getId()); | ||
| throw $e; | ||
| } | ||
| } | ||
|
|
||
| if ($this->getSupportForDatabaseStatus()) { | ||
| $this->provisioningDatabases[$resource->getId()] = true; | ||
| } | ||
|
|
||
| return true; | ||
| })(), | ||
| SchemaAction::Create => null, | ||
|
|
@@ -611,7 +738,7 @@ protected function createDatabase(Database $resource): bool | |
| } | ||
| } | ||
|
|
||
| $database = $this->dbForProject->createDocument(self::META_DATABASES, new UtopiaDocument([ | ||
| $document = [ | ||
| '$id' => $resource->getId(), | ||
| 'name' => $resource->getDatabaseName(), | ||
| 'enabled' => $resource->getEnabled(), | ||
|
|
@@ -622,25 +749,45 @@ protected function createDatabase(Database $resource): bool | |
| 'type' => empty($resource->getType()) ? 'legacy' : $resource->getType(), | ||
| // Resolved by the destination's resolver (or left blank); never copy the source's DSN by default. | ||
| 'database' => $this->resolveDestinationDsn($resource), | ||
| ])); | ||
| ]; | ||
|
|
||
| // Only SOURCE_DATABASE Appwrite migrations manage the status field: create in `provisioning`, | ||
| // then markProvisionedDatabasesReady() flips it to `ready` once the run finishes. Every other | ||
| // source leaves status untouched so the collection default applies. Never copy the source's state. | ||
| if ($this->getSupportForDatabaseStatus()) { | ||
| $document['status'] = self::DATABASE_STATUS_PROVISIONING; | ||
| } | ||
|
|
||
| $database = $this->dbForProject->createDocument(self::META_DATABASES, new UtopiaDocument($document)); | ||
|
|
||
| $resource->setSequence($database->getSequence()); | ||
|
|
||
| $columns = \array_map( | ||
| fn ($attr) => new UtopiaDocument($attr), | ||
| $this->collectionStructure['attributes'] | ||
| ); | ||
| try { | ||
| $columns = \array_map( | ||
| fn ($attr) => new UtopiaDocument($attr), | ||
| $this->collectionStructure['attributes'] | ||
| ); | ||
|
|
||
| $indexes = \array_map( | ||
| fn ($index) => new UtopiaDocument($index), | ||
| $this->collectionStructure['indexes'] | ||
| ); | ||
| $indexes = \array_map( | ||
| fn ($index) => new UtopiaDocument($index), | ||
| $this->collectionStructure['indexes'] | ||
| ); | ||
|
|
||
| $this->dbForProject->createCollection( | ||
| $this->databaseCollectionId($database), | ||
| $columns, | ||
| $indexes | ||
| ); | ||
| $this->dbForProject->createCollection( | ||
| $this->databaseCollectionId($database), | ||
| $columns, | ||
| $indexes | ||
| ); | ||
| } catch (\Throwable $e) { | ||
| // The metadata document exists but the database isn't usable; mark it failed before propagating. | ||
| $this->markDatabaseFailed($resource->getId()); | ||
| throw $e; | ||
| } | ||
|
|
||
| // Fully created; when we manage status it stays provisioning until the end-of-run sweep flips it to ready. | ||
| if ($this->getSupportForDatabaseStatus()) { | ||
| $this->provisioningDatabases[$resource->getId()] = true; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
@@ -3271,6 +3418,7 @@ protected function createRule(Rule $resource): bool | |
| 302 => StatusCode::FOUND(), | ||
| 307 => StatusCode::TEMPORARYREDIRECT(), | ||
| 308 => StatusCode::PERMANENTREDIRECT(), | ||
| // no break | ||
| default => StatusCode::MOVEDPERMANENTLY(), | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| }; | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.