Conversation
Published 2.0 still used Database::VAR_* and positional createAttribute/createIndex, which feat-query-lib removed. Rebase onto main and pass Attribute/Index/Relationship VOs plus ColumnType/IndexType so Appwrite can pin this branch as 2.0.0.
Appwrite stores ColumnType::BigInteger as biginteger. CSV export resolved that as an unsupported column type and wrote no rows.
createDocument can return an empty Mongo sequence while a subsequent
getDocument has the ObjectId. Creating database_{seq} from the create
return left table import looking up a collection that did not exist.
Appwrite main added huggingface as a project OAuth2 provider. Without an allow-list entry, Appwrite-to-Appwrite migrations fail on that provider even when the rest of the transfer succeeded.
Keep query-lib APIs and the huggingface PROVIDERS allow-list already on main.
Appwrite #11649 locks database at 5719edd. Staying on e593b78 would only prove the schema VO calls against an older query-lib surface.
|
utopia-php/database feat-query-lib keys silenced events with Coroutine::getCid(). The CI image is vanilla PHP, so Memory-adapter tests fatalled before they could run.
createDocument can persist a row whose subsequent getDocument is empty. That throw sat outside the failed-status handler, so a later skip could flip the unusable database to ready without a backing collection.
|
Addressed the reload-failure finding.
Reload + Also stubbed @greptile-apps review |
Asterisk wildcards on utopia-php packages are replaced with equivalent caret constraints so Composer ranges stay consistent.
Keep composer.json and composer.lock in sync so `composer validate` passes, and pin utopia-php/database to the current query-lib HEAD.
|
@greptileai review |
|
@greptile-apps review Force re-review of HEAD |
Database::createCollection no longer accepts a string id.
Database::checkAttribute now requires Attribute. Build schema models from the resource key so metadata document IDs are not used as attribute keys.
Appwrite E2E migrations failed because checkAttribute now requires Attribute, and the destination still handed it a metadata Document.
A reload failure leaves a metadata document in `failed` with no backing collection. Recovery only ran when onDuplicate was not Fail, so the default policy retried createDocument against the existing ID and stranded the database.
|
@greptileai review |
Index types already used IndexType; column direction was still a raw ASC string. Collection constructors with multiple named params were also jammed on one line.
…atabase The lock held utopia-php/query at dev-feat-schema-order, a branch that no longer exists on the remote, so the resolution only survived as long as nobody resolved it again. database's branch requires query 0.6.*, which is released, so this takes the release. storage 4.0.4 comes along because 4.0.3 capped utopia-php/validators at ^0.4 while database requires ^0.5; 4.0.4 dropped the validators dependency outright. database has required ^0.5 on main as well as on the branch, so this was already true before the query-lib work and only surfaced now that the lock moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The database library no longer carries a per-document version, so the compare-and-set on `expectedVersion` has nothing to compare. The ownership writes now run under withRequestTimestamp with the timestamp the row was read with: every update moves `$updatedAt` strictly forward, so a row another writer touched between the locked read and the write is refused as a conflict exactly as before, and the rows are still locked with forUpdate inside the claim transaction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…head Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every source wraps each resource type's export in a catch-all that records the failure and moves on to the next type. An exception thrown by the progress callback was recorded the same way, so a consumer could not stop a transfer: appwrite's worker throws Superseded from the callback once its migration attempt is superseded (a zombie worker whose row was expired and retried, or a DELETE on a running migration), and the worker kept importing the first batch of every remaining type into databases the new attempt had already claimed. Add Utopia\Migration\Exception\Aborted and rethrow it from each of those catches, including the per-file and per-deployment catches nested inside the storage, functions and sites exports, so nothing further is exported or imported and the abort leaves Transfer::run() unchanged. It extends RuntimeException rather than Utopia\Migration\Exception, which is the per-resource error record the sources append to; consumers extend it (appwrite's Superseded) to keep their own type. Every other failure is still recorded as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library's sources now rethrow Aborted, but a source outside the library, or a catch added later, can still record it and carry on, and Transfer::run() would then return as if the transfer had finished. Transfer now keeps the first abort its callback throws: later batches rethrow it without calling the callback again, and the run ends with it, so the caller always sees the abort. The state is per run, so a later run on the same Transfer is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lares them A destination whose `databases` metadata has `status` but not yet `migrationId`/`migrationAttemptId` (a project Appwrite's V25 reached and V26 has not, the normal state of a rolling upgrade) passed the only probe, which looked for `status`, so every database write carried the owner too. Where unknown attributes are rejected the create failed with `Unknown attribute: "migrationId"`; where they are dropped, as Appwrite's project database does, the owner vanished and the ready flip refused every database as "owner changed before finalization". Either way no database, and nothing under it, arrived. Ownership now has its own probe that requires both owner attributes. Owners are written, and the ready/failed flips check them, only when the schema declares them; a status-only schema gets status tracking exactly as before ownership existed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…existed A run on the released library that died mid-migration leaves its database `provisioning` or `failed` with no owner, because nothing wrote one then. Every later attempt refused that row before any recovery logic ran: no attestation can vouch for an owner that was never recorded, so the database stayed unrecoverable until someone edited the metadata by hand. On main this was the supported recovery path. A row that names no owner at all is recovered as it was before ownership existed: a failed database is overwritten, and a provisioning one is resolved like any existing database under the OnDuplicate policy (Fail skips rather than colliding with it) and re-enrolled so the ready flip completes it. Either way the current attempt claims it under the row lock where the schema can record an owner, so a concurrent attempt sees it owned and needs attestation. A missing backing collection is recreated first in both cases, so a database stranded before its collection existed is never flipped to ready without one. The attestation callback is still consulted, and still fails closed, for every row that names an owner, including one naming only half of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status flips run outside any transaction, so what keeps a stale writer out is the pin to the `$updatedAt` it read, which holds only while every write moves that timestamp strictly forward. The overwrite claim wrote the source archive's `$updatedAt` into that column, usually older than the row it replaces; it stayed harmless only because createDatabase() happens to run with preserveDates off. Under the run-wide preserveDates the claim moved the timestamp backwards and a writer pinned to the pre-claim read passed the fence. The claim no longer supplies `$updatedAt`, so the library always derives a strictly later one, and every guarded write re-reads the row and fails when the stored timestamp did not move past the one it was pinned to. Inside the claim transaction that failure rolls the claim back, so the invariant is enforced here rather than inherited from a setting several frames up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce has none `updateRelationship()` reads a null action as "leave it unchanged", but an overwrite of a two-way relationship whose source carried no `onDelete` passed `restrict` instead, silently rewriting the destination's `cascade` or `setNull` and with it the delete semantics of the migrated data. An empty or unknown value from a hand-edited archive threw a ValueError, failing the column. The source action is parsed with tryFrom(), so a missing or unusable value reaches the library as null, and both sides of the Appwrite attribute metadata keep the destination's action instead of recording a value the library never applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
success() returned before cleanupOverwriteOrphans() as soon as one provisioned database failed its ready write, so a single transient failure in a multi-database OnDuplicate::Overwrite migration left the unswept columns and indexes of every table behind, not just those of the database that failed. The two steps are independent. The flip still runs first, so a sweep failure can't strand databases in provisioning, and each flip failure is still recorded with addError(); the sweep now always follows, as it did on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
success() reported a database stuck in provisioning only by appending to getErrors(), after the point where most callers gate on errors: a caller that checks once, before success(), marked the migration completed while its databases stayed unusable. A thrown exception cannot be missed that way, and the callers already wrap the lifecycle in a try/catch. success() still attempts every step first: each flip failure and each table whose overwrite sweep fails is recorded with addError(), so the report keeps per-resource detail, and a failing table no longer stops the sweep of the tables after it. It then throws Utopia\Migration\Exception\Finalization carrying those failures. The exception extends Utopia\Migration\Exception so callers that bubble library errors keep treating it as one. The contract is documented on Target::success(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finalization moved from run() into success(), which neither Transfer nor Target calls. A consumer that kept $transfer->run() as its last call left every imported database in provisioning and every overwrite sweep undone with no error and no failed resource, and a reused destination silently discarded the previous run's pending work on its next run(). Finalization stays in success(), because callers such as appwrite must persist their finalizing claim before it runs, so the skip is reported where it can no longer be a false alarm: Transfer calls no lifecycle hook, and shutdown() precedes success() in the caller's sequence, so the destination tracks whether a returned run is still pending and records an error for each database left unfinalized when cleanUp() ends the lifecycle, or when run() starts over. error() acknowledges a run that will not be finalized and success() settles it, so neither path reports anything. success() now finalizes only a run that returned, as run() did on main: an interrupted run keeps its databases in provisioning for a later attempt to recover instead of exposing them as ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock now points at d82a7ec54, the head carrying the review fixes this branch's own changes were written against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
success() skipped finalization only when the destination's own run() threw. A source that catches Aborted and carries on lets run() return, which sets the pending finalization, and only Transfer::run() then rethrows the latched abort. A caller that called success() after that would flip the partially imported databases to ready and sweep their overwrite orphans. Transfer now tells the destination the run was aborted, through a new no-op Destination::markAborted(), just before it rethrows. The Appwrite destination drops its pending finalization there, so a later success() is a no-op exactly as it already is after a run that threw, and the databases stay in provisioning for a later attempt. error() is not used for this: a custom destination's error() may do more than skip finalization, and the caller still chooses between error() and a retry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…repositories The inline alias `dev-feat-query-lib as 7.0.0` only ever worked in a root package: in a dependency's require, Composer strips the ` as 7.0.0` half and the requirement becomes the literal `== dev-feat-query-lib`. Together with the dependency-level `repositories` block, which Composer reads only from the root package, a published utopia-php/migration was installable only by a consumer that replicated the same three VCS entries in its own composer.json, and it would have kept demanding the branch itself once appwrite moved to a tag. database now comes from Packagist as `8.*`, served by its `8.0.x-dev` branch alias, so the three VCS entries are gone and query and async resolve from Packagist at the same commits the lock already carried. The remaining caret constraints with a `.0` lower bound become wildcards; `halaxa/json-machine` keeps `^1.2` because no wildcard says the same. `minimum-stability: dev` and `prefer-stable: true` stay until database 8.0.0 is tagged, and the release sequence restores `stable`. The `branch-alias` extra makes the branch resolvable as the next release, 3.0, for anyone who tracks it before the tag exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
migration has never carried a changelog, and 3.0 is the first release that breaks its consumers: the database 8 requirement, the Appwrite destination's owner arguments and provisioning lease, the standalone CLI's new required options, the success()/error() finalization contract and Exception\Aborted. Collecting them with the upgrade notes gives consumers one place to read before they move off 2.x. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Appwrite destination wrote the schema enum value into the project's `attributes` metadata, so a migrated big integer landed as `biginteger` while every other writer in Appwrite stores `bigint`. Appwrite's attribute-update endpoints compare that raw string, so such a column could only be changed by dropping and recreating it, losing its data. `schemaColumnType()` now returns `Attribute::persistedType()` of the matching `ColumnType` case: only the big-integer case changes value, every other type keeps the spelling it had. The two sites in `createField()` that turn that string back into a `ColumnType` go through `Attribute::normalizeType()`, which accepts both spellings, so the table's physical column is still `ColumnType::BigInteger`. The raw comparisons against stored metadata now line up on both sides. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it is stale On destination metadata that declares `status` but not `migrationId` and `migrationAttemptId`, every database row a migration writes names no owner, so `getRecoverableOwner` has nothing to judge and the ownerless branch recovered the row straight away. Two migrations of the same database then race: the second re-enrolled a `provisioning` row and flipped it `ready` in its own `success()` while the first was still writing the schema, and overwrote a `failed` row an active attempt had just recorded. The owned path refuses both without terminal attestation. With no owner to consult, the row's update timestamp stands in for one: a `provisioning` or `failed` row that names no owner is recoverable only once it has gone a whole provisioning lease without a write, and a fresher one fails closed with an error that names the database and says when it becomes recoverable. Staleness is checked twice, like the owner: on the row read before the claim and again on the row locked inside the claim transaction, so a recovery that lands in between is not taken over. Timestamps are compared as absolute epoch seconds against the destination's own clock, never against a source-supplied one. The lease is a trailing `provisioningLease` constructor argument defaulting to 86400 seconds -- Appwrite's `Claim::FINALIZING_LEASE`, the longest a live attempt holds its lease. An ownerless row is not refreshed while its migration imports, so a shorter default could hand a live row over. `0` restores the immediate recovery and a negative lease is rejected. Rows written before ownership existed are older than any lease, so they recover as before, and rows that name an owner are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hangelog The 3.0.0 entry described the provisioning lease from mg-05's side before mg-07 landed the implementation, so two clauses of contract C10 were weaker than the code and than README's recovery section: a negative lease is rejected with an InvalidArgumentException, and a row still inside the lease makes createDatabase() fail closed with an error that names the database. Both matter to a caller deciding what to catch and what the message will say, so the changelog now matches the code rather than the other way round. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…w is claimed On destination metadata that declares `status` but no owner attributes, an ownerless `provisioning` row whose spec already matches takes `SchemaAction::Skip`, so the claim transaction had nothing to write and left `$updatedAt` where it was. The lease is measured from that timestamp, so a migration arriving a second after a recovery read the same stale row, passed both staleness checks and recovered the database a live migration was already importing into. The claim now always touches an ownerless row. Repeating the stored status is not a change, so the write carries the refreshed `$updatedAt` itself; `updateOwned()` then asserts the timestamp moved forward, which is what refuses the second claim. README and CHANGELOG already state the rule as a lease measured from the last write; this makes that true of the `provisioning` recovery path as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…column Two comparisons against Appwrite's stored attribute metadata still matched the type as a raw string. A row written as `biginteger` by this branch's dedicated big-integer endpoint therefore never matched a desired `bigint`: the spec check refused to skip, the in-place update refused an immutable-field change, and the caller fell through to dropping and recreating the column, taking its data with it. Appwrite itself accepts both spellings for the same column type, so the destination now compares them the same way and leaves the column alone. The type moves out of ATTRIBUTE_IMMUTABLE_FIELDS into its own normalised check; it stays immutable — only its comparison changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The live row was seeded one second short of the 60-second lease it is checked against, so a runner that paused between seeding and the claim would see it lapse and the refusal it asserts would disappear. A second-old row exercises the same boundary with the whole lease as cushion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Transfer calls it only on the latched path, when a source swallowed the abort and run() returned; an abort that propagates straight out of run() never reaches it. The docblock read as unconditional, which would invite a custom destination that writes terminal state during run() to rely on a call it will not get. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 24-hour default appeared as a bare literal in the constructor, in the test helper that mirrors it and in every seed that has to sit past it, so the four could drift apart silently. DEFAULT_PROVISIONING_LEASE names it once and the seeds derive from it. The literal inside the asserted error messages stays: that half is the observable contract and should be pinned independently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 3.0.0 upgrade note for custom destinations told implementers what to override but not when the hook fires. A destination author reading only the upgrade notes could reasonably assume markAborted() runs for every abort, including one that propagates straight out of run(), and build cleanup that silently never happens on that path. State the latched-abort precondition there, matching the markAborted() docblock in src/Migration/Destination.php and the "Added" entry earlier in the same release. The ATTRIBUTE_IMMUTABLE_FIELDS note was a single 217-column line, unreadable without horizontal scrolling and inconsistent with every other multi-line docblock in the file. Wrap it under 120 columns; the wording is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The train wrote its Composer constraints as wildcards under a house rule that has since been withdrawn, so they are carets again. appwrite/appwrite takes main's and 2.0.7's form, ^27.0, again. utopia-php/database, which the train moved from main's ^7.0.0 to 8, takes the caret of that range, ^8.0. With minimum-stability dev it is still served by the database branch's 8.0.x-dev alias until 8.0.0 is tagged. Each caret parses to exactly the range of the wildcard it replaces. utopia-php/storage 4.* and utopia-php/dsn 0.2.* stay as they are: they are main's own constraints, already in 2.0.7, and the train never changed them. The lock changes only its content-hash because every locked version satisfies the carets (appwrite/appwrite 27.1.0, utopia-php/database dev-feat-query-lib at 8b716e3c1c); no package moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Appwrite main now registers a Webflow OAuth2 provider, and migrating projects that use it requires the 'webflow' allow-list entry released in 2.0.8 (#228). Merging main keeps the query-lib train compatible with appwrite main instead of rejecting Webflow providers during migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adapts the migration destination's schema calls to the query-lib value objects.
Why this approach
Database::createCollection()takes aCollectiononly, so destination Appwrite now wraps the collection id, attributes and indexes — along with the named permissions anddocumentSecurity— innew Collection(...)instead of passing positional arrays.utopia-php/databaseis pinned todev-feat-query-lib as 2.0.0, re-pinned to that branch's head whenever it moves.Lock repairs that came with the re-pin
Two problems surfaced only when the lock was resolved again rather than reused:
utopia-php/querywas locked todev-feat-schema-order, a branch that no longer exists on the remote. The resolution survived only as long as nobody resolved it. database's branch requiresquery 0.6.*, which is released, so the lock now takes the release.utopia-php/storagemoved 4.0.3 → 4.0.4, because 4.0.3 cappedutopia-php/validatorsat^0.4while database requires^0.5. 4.0.4 dropped the validators dependency outright. database has required^0.5onmainas well as on the branch, so this was already true before the query-lib work and only surfaced now.A live bug the re-pin exposed
The Appwrite destination passed the
'ASC'/'DESC'strings a source hands back straight intoUtopia\Database\Index, which takesOrdercases and rejects anything else. The resultingInvalidArgumentExceptionis not a MigrationException, so instead of recording a failed index the whole transfer aborted.This was already true before this PR and simply could not be seen: the lock held
queryat the deleted branch, whoseIndextook plain strings, so CI had never built an index against the contract the released library actually has.AppwriteIndexLengthsTestcovers it — two of its cases pass['ASC', 'ASC'], and both went red on the first run against the re-pinned lock and green with the fix.It is the same defect as the one in appwrite/appwrite#11649's
Databasesworker, from the same cause.Also on this branch: provisioning ownership and recovery
The branch has grown well past its title. Alongside the value-object adaptation it carries a provisioning-owner / lifecycle-fencing subsystem added 2026-08-29..31, which does not depend on query-lib at all:
Destinations/Appwrite/ProvisioningOwner.phpand owner persistence inDestinations/Appwrite.phpbin/MigrationCLI.phpAppwriteDatabaseConcurrencyTest(+400),AppwriteDatabaseStatusTest(+1026),MigrationCLITest(+342),ProvisioningOwnerTest(+36)Total diff against
mainis +3450/-714 over 23 files. Recommendation: split those commits into a PR againstmainso they can ship now, and leave only theCollection/Attribute/Index/Orderadaptation and the lock re-pin here, where it is blocked on database#823 being tagged.Chain
Landing order, bottom up:
The three framework PRs once stacked on #823 — #947 (ORM), #948 (repositories and seeding), #949 (migration runner and schema differ) — are closed and are not part of this train.
Verified
Not verified
Destinations/Appwrite.phpregions; whichever lands second will need a merge.utopia-php/databasedependency is still a branch pin. It becomes a released tag only once #823 merges, and this PR should not land before that.Fixes from the 2026-09-23 review
A progress callback can stop a transfer:
Exception\Aborted(11.7)Utopia\Migration\Exception\Aborted(extendsRuntimeException).Transfer::run()orTransfer::runWithResourceSelector()to stop the transfer.records a resource type's failure and moves on. Nothing further is exported or imported.
run()/runWithResourceSelector()as the same instance.getErrors()as before.a batch.
and group. It imported the first batch of each before the callback threw again.
superseded.
it too. An abort no longer skips to the next file or deployment.
Transferkeeps the first abort for the rest of the run. A source outside this library that still catches it andcarries on cannot turn it into a normal return: later batches rethrow it without calling the callback again, and the
run ends with it. A later
run()on the sameTransferis unaffected.Abortedis deliberately not aUtopia\Migration\Exception. That type is the per-resource error record that sourcesappend to
errors, and an abort is not a resource error.Provisioning ownership (06.1, 06.2, 06.7, 06.9)
Owners are written only where the schema has room for them.
status(Appwrite V25) and for themigrationId/migrationAttemptIdowner pair (V26).ownership existed.
returned
Unknown attribute: "migrationId". Schemas that drop them, like Appwrite's project database, left everydatabase stuck in
provisioningwith "owner changed before finalization".Databases left incomplete before ownership existed can be recovered again. A
provisioningorfaileddatabasethat names no owner was left by an earlier library version or by a status-only schema. It is recovered without
getRecoverableOwner, as it was before ownership existed:faileddatabase is overwritten.provisioningdatabase is resolved under theOnDuplicatepolicy like any existing database, andfailkeeps itinstead of colliding with it. It is marked
readywhen the run succeeds.Either way, the current attempt claims the database under the row lock when the schema can record an owner. A missing
backing collection is recreated before the ready flip. A database that names an owner, even half of one, still needs
attestation, and a refusal still fails closed.
Provisioning writes are fenced without borrowed timestamps.
$updatedAtinto the column the optimistic fencecompares. So
$updatedAtalways moves forward, whatever thepreserveDatessetting.(
Database <id> provisioning write did not move its update timestamp forward). Inside the claim transaction it isrolled back.
A relationship overwrite no longer changes an
onDeletethe source didn't specify.onDelete, or an empty or unknown one, the value reachesupdateRelationship()asnull("unchanged").
restrict, or an empty or unknown value threw aValueError.Finalization contract (06.3, 06.4, 06.8)
Destinations\Appwrite::success()used to report a database it could not markreadyonly by appending togetErrors(). That came after the point where most callers check for errors, so a caller that checked once beforesuccess()marked the migration completed while its databases stayedprovisioning.success()now attempts every step, records each failure withaddError()as before, and then throwsUtopia\Migration\Exception\Finalization.Utopia\Migration\Exception, and itsfailuresproperty lists the recorded errors.readywrite madesuccess()return before theOnDuplicate::Overwriteorphansweep. That left the unswept columns and indexes of every table behind.
provisioning. The sweep now alwaysfollows, as it did before finalization moved to
success().success()is reported.success(), because callers such as Appwrite must persist their finalizing claim before itruns. Neither
TransfernorTargetcallssuccess(), andTransfercalls no lifecycle hook at all.cleanUp()ends the lifecycle, or whenrun()starts again on the same instance.success(), orerror()for a run it won't finalize, gets no report.success()is due, so the error check a caller makes betweenrun()andsuccess()still passes.
success()after a destinationrun()that threw now does nothing. Thedatabases stay
provisioningfor a later attempt to recover, as they did whenrun()finalized itself. Before thischange they were marked
readywith only part of their contents.Target::success(),Target::error()andTarget::cleanUp().Upgrade notes
Sourcesubclass that catches\Throwablearound its exports should rethrowUtopia\Migration\Exception\Abortedbefore recording other errors:} catch (Aborted $abort) { throw $abort; } catch (\Throwable $error) { ... }.Transfer::run()still ends with theabort if it does not, but the source keeps exporting until it returns.
Destinations\Appwrite:run()returns, persist any claim that must come first, then callsuccess().Exception\Finalizationfromsuccess(). Each failure is also ingetErrors().error()instead.AbortedfromTransfer::run(), callerror()(or nothing), neversuccess().getRecoverableOwneris unchanged. It is now called only for rows that name an owner, so existing callbacks needno change.
CHANGELOG highlights (3.0.0, unreleased)
mg-05 writes
CHANGELOG.mdin wave 2; these are the entries it should carry.mainrequires^7.0.0). The PHP floor is unchanged:>=8.5, ason
main.ProvisioningOwner $owner, made from a stable logical migration id and a fresh attempt idfor every execution.
getRecoverableOwnercallback that attests the terminal owner of an incomplete database.--migration-idand a fresh--migration-attempt-id. Recovering an incompletedatabase that names an owner also requires
--recover-migration-idand--recover-migration-attempt-id.databasesmetadata declares both owner attributes.Databases that name no owner are recovered as before.
Destinations\Appwrite::run()no longer finalizes.success()oncerun()has returned (after persisting any claim that must come first), orerror()for arun you won't finalize.
success()throwsUtopia\Migration\Exception\Finalizationwhen a step fails. Each failure is also recorded withaddError().cleanUp()records an error for each database left unfinalized when neither was called.Utopia\Migration\Exception\Aborted. Throw it from a transfer's progress callback to stop the transfer.Every library source rethrows it, and
Transferends the run with it.statusbut no owner attributes.provisioningorfailedwithout an owner.onDeletewhen the source has none.Commits
🤖 Generated with Claude Code