Target::call()no longer follows redirects; a 3xx response throws.setFollowRedirects(true)restores following. Requests are limited tohttpandhttps, redirects included.- The Appwrite source's SDK client is
Sources\Appwrite\Client, a subclass of the SDK client, and it never follows redirects: a 3xx response throws, except forlocationresponses, which return the redirect target. - Requires
appwrite/appwrite^30.4.0and no longer requiresutopia-php/dsn, which nothing used.
NHost::getDatabaseHost()returns the Postgres host the source connects to (Supabase returns its configured host), so a caller can check where it resolves before connecting.NHost::setDatabaseAddress()connects to the database host on a given address (libpqhostaddr), so the connection lands on the address a caller checked.NHost::getDatabaseDsn()returns the DSN it connects with.Target::setDestinations()confines a target's requests to aUtopia\Client\Destinations. Each connection and redirect hop is checked against the address it actually reached, so DNS cannot answer differently between a check and the connection. The Appwrite source applies it to its SDK client too. Unset, requests connect anywhere.Target::setResolver()pins each request to the addresses a resolver returns asCURLOPT_RESOLVEentries, and keeps pinned requests off proxies. The Appwrite source applies it to its SDK client too.- Appwrite to Appwrite migrations carry what sdk-for-php 30.4 can write: membership IDs, topic
qosandexpiry, the reply-to address of scheduled emails, the Android channel of scheduled pushes, the password-pwned, password-rotation and passkey policies, and project environments as the newproject-environmentresource.
- The CSV destination prefixes string values that start with
=,+,-,@, a tab or a carriage return with a single quote, so spreadsheet applications do not evaluate them as formulas. Numbers, booleans and arrays are written as before. The CSV source strips the quote again, so exports import back unchanged.
3.1.2 reverses the 3.0.0 notes below on Aborted, markAborted() and finalizing with success().
- Requires
utopia-php/database^8.1.2(3.1.1 requires^8.0), which moved the update models into the namespace of the model they update. - Provisioning ownership fencing is removed. The Appwrite destination tracks a database's lifecycle with
statusalone again, but recovers incomplete databases differently from 2.x.Destinations\Appwrite\ProvisioningOwneris removed.Destinations\Appwrite::__construct()no longer takesowner,getRecoverableOwnerorprovisioningLease, andDestinations\Appwrite::DEFAULT_PROVISIONING_LEASEis removed.onDuplicateand the arguments after it move up two places for positional callers.- The destination no longer reads or writes the
migrationIdandmigrationAttemptIddatabase attributes. - A
provisioningorfaileddatabase is recovered by the next run at once, without owner attestation or a lease wait. It is always overwritten from the source, whateverOnDuplicateis and whatever the timestamps say. 2.1.0 forced the overwrite only for afaileddatabase and never underOnDuplicate::Fail; it skipped aprovisioningdatabase whose spec matched the source and only re-enrolled it for thereadyflip.
- The standalone CLI no longer accepts
--migration-id,--migration-attempt-id,--recover-migration-id,--recover-migration-attempt-idor-h/--help.MigrationCLI::__construct(array $arguments)andMigrationCLI::getHelp()are removed. Destinations\Appwrite::run()finalizes again, as in 2.x: once the transfer returns it flips the databases it provisioned toready, then runs the overwrite orphan sweep.success(),error()andcleanUp()are no-op lifecycle hooks again. Callers no longer callsuccess()to finalize, andcleanUp()no longer reports a skippedsuccess().- A failed
readyflip is recorded withaddError(), and the sweep still runs. - The orphan sweep stops at the first table that fails, and
run()throws that table's database exception as is. 3.1.1'ssuccess()tried every table, recorded each failure withaddError()and threwFinalization. Utopia\Migration\Exception\Finalizationis removed.
- The abort mechanism is removed.
Utopia\Migration\Exception\AbortedandDestination::markAborted()are removed.Transfer::run()no longer latches an exception thrown from its progress callback, and the library sources no longer rethrow one: each source handles it as in 2.x.
- The Appwrite destination treats every
provisioningorfaileddatabase as abandoned by an earlier run. Callers must serialise migrations per destination project. A concurrent run overwrites another run's incomplete databases from its own source under anyOnDuplicate, and may flip them toreadybefore the other run finishes.
3.0 is a major release. The breaking changes below all concern the Appwrite destination's lifecycle and the database library it builds on; read the upgrade notes before you upgrade from 2.x.
- Requires
utopia-php/database8 (mainrequires^7.0.0). The PHP floor is unchanged:>=8.5. - The Appwrite destination records which migration attempt provisions each database.
Destinations\Appwrite::__construct()takes aProvisioningOwner $owner, made from a stable logical migration id and a fresh attempt id for every execution.- It also takes a
getRecoverableOwnercallback that attests the terminal owner of an incomplete database. - It takes a trailing
int $provisioningLease, 86400 seconds by default. A negative value is rejected with anInvalidArgumentException. - Owners are written and enforced only where the destination's
databasesmetadata declares both owner attributes.
- Recovering an incomplete database is gated.
- A
provisioningorfaileddatabase that names an owner is recovered only whengetRecoverableOwnerattests that owner. - A
provisioningorfaileddatabase that names no owner is recovered only once it has not been updated for the destination's provisioning lease. A fresher one is refused:createDatabase()fails closed with an error naming the database, because another migration may still be provisioning it.provisioningLease: 0recovers it at once, which is how 2.x behaved. - The standalone CLI requires
--migration-idand a fresh--migration-attempt-id. Recovering a database that names an owner also requires--recover-migration-idand--recover-migration-attempt-id.
- A
Destinations\Appwrite::run()no longer finalizes.- Call
success()oncerun()has returned, after persisting any claim that must come first, orerror()for a run you will not finalize. success()throwsUtopia\Migration\Exception\Finalizationwhen a step fails. Every step is attempted and each failure is also recorded withaddError().cleanUp()records an error for each database a returned run left unfinalized when neithersuccess()norerror()was called.
- Call
Utopia\Migration\Exception\Aborted. Throw it from a transfer's progress callback to stop the transfer. Every library source rethrows it, andTransferends the run with it.Destination::markAborted(), a no-op by default.Transfercalls it before it rethrows a latched abort. The Appwrite destination drops its pending finalization there, so a latersuccess()finalizes nothing.
- Imports into a destination whose database metadata has
statusbut no owner attributes. - Recovery of databases left
provisioningorfailedwithout an owner. - An ownerless database that another migration may still be provisioning is no longer taken over.
- The provisioning fence no longer relies on source timestamps.
- A relationship overwrite keeps the destination's
onDeletewhen the source has none. - One failed ready flip no longer skips the overwrite orphan sweep.
success()after a transfer that ended with an abort finalizes nothing, whatever the source did with the abort.
- Custom sources. A
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 the abort if it does not, but the source keeps exporting until it returns. - Callers of
Destinations\Appwrite:- After
run()returns, persist any claim that must come first, then callsuccess(). - Catch
Exception\Finalizationfromsuccess(). Each failure is also ingetErrors(). - For a run you will not finalize, call
error()instead. - After catching
AbortedfromTransfer::run(), callerror()(or nothing), neversuccess(). SinceTransferreports the abort to the destination, a mistakensuccess()is harmless.
- After
getRecoverableOwneris unchanged. It is now called only for rows that name an owner, so existing callbacks need no change.- Custom destinations inherit
markAborted()as a no-op. Override it only to drop state a latersuccess()would otherwise commit;Transfercalls it with the abort still in flight. It runs only for an abort a source recorded andTransferlatched — an abort that propagates straight out ofrun()never reaches it.