Skip to content

feat: adapt migration schema calls to query-lib value objects - #222

Open
abnegate wants to merge 19 commits into
mainfrom
feat-query-lib
Open

feat: adapt migration schema calls to query-lib value objects#222
abnegate wants to merge 19 commits into
mainfrom
feat-query-lib

Conversation

@abnegate

@abnegate abnegate commented Aug 21, 2026

Copy link
Copy Markdown
Member

Adapts the migration destination's schema calls to the query-lib value objects.

Why this approach

Database::createCollection() takes a Collection only, so destination Appwrite now wraps the collection id, attributes and indexes — along with the named permissions and documentSecurity — in new Collection(...) instead of passing positional arrays.

utopia-php/database is pinned to dev-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/query was locked to dev-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 requires query 0.6.*, which is released, so the lock now takes the release.
  • utopia-php/storage moved 4.0.3 → 4.0.4, 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.

A live bug the re-pin exposed

The Appwrite destination passed the 'ASC' / 'DESC' strings a source hands back straight into Utopia\Database\Index, which takes Order cases and rejects anything else. The resulting InvalidArgumentException is not a Migration Exception, 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 query at the deleted branch, whose Index took plain strings, so CI had never built an index against the contract the released library actually has. AppwriteIndexLengthsTest covers 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 Databases worker, from the same cause.

Chain

Landing order, bottom up:

  1. utopia-php/database#823 — the query-lib migration itself
  2. utopia-php/abuse#124, utopia-php/audit#133, utopia-php/migration#222 — the schema call sites in the libraries
  3. appwrite/appwrite#11649
  4. appwrite-labs/cloud#5410

Stacked on #823 but not part of it, and not required by anything above: #947 (ORM), #948 (repositories and seeding), #949 (migration runner and schema differ).

Every dev-feat-query-lib pin in this train is re-pinned to its branch head whenever one of them moves, so each PR's CI runs against what the others actually contain.

Verified

  • CI green on this head
  • Greptile 5/5, no unresolved threads
  • Pint on the Collection wrap

Not verified

  • The utopia-php/database dependency is still a branch pin. It becomes a released tag only once #823 merges, and this PR should not land before that.

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.
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adapts migration schema handling to query-library value objects and updates the database dependency while repairing retry behavior for incomplete Appwrite database provisioning.

  • Wraps destination collection schemas in Collection, Attribute, Index, and relationship value objects.
  • Converts schema constants and source values to query-library enum types.
  • Recovers both failed and provisioning database metadata under the default duplicate policy.
  • Updates dependency resolution and adds regression coverage for status recovery and index ordering.

Confidence Score: 5/5

The PR appears safe to merge, with no blocking failure remaining in the previously reported database-retry paths.

The current recovery path recognizes both failed and provisioning metadata, forces overwrite recovery under the default duplicate policy, recreates a missing backing collection using the persisted sequence, and schedules the recovered database to become ready after the run.

Important Files Changed

Filename Overview
src/Migration/Destinations/Appwrite.php Adapts schema creation to value objects and completes retry recovery for database metadata left in failed or provisioning states.
tests/Migration/Unit/Destinations/AppwriteDatabaseStatusTest.php Adds regression coverage showing default duplicate-policy retries recover failed and stranded provisioning metadata.
tests/Migration/Unit/Destinations/AppwriteIndexLengthsTest.php Covers conversion of source index-order strings to query-library Order cases while retaining index lengths.
composer.json Temporarily pins the query-library database branch and enables stable-preferred development dependency resolution.
composer.lock Resolves the database branch and its updated query, storage, validator, cache, and asynchronous dependencies.

Reviews (13): Last reviewed commit: "Merge branch 'main' into feat-query-lib" | Re-trigger Greptile

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.
@abnegate

Copy link
Copy Markdown
Member Author

Addressed the reload-failure finding.

createDocument can persist a _databases row whose immediately following getDocument is empty (the Mongo sequence miss this branch already reloads for). That throw sat outside the markDatabaseFailed catch, so the document stayed provisioning and a later spec-matching skip could flip it to ready with no backing collection.

Reload + createCollection now share that catch. testReloadFailureMarksTheDatabaseFailed fails without the wrap (provisioning) and passes with it (failed).

Also stubbed Swoole\Coroutine::getCid() in the PHPUnit bootstrap so the Memory-adapter suite can run on the CI image, which has no Swoole extension. utopia-php/database feat-query-lib keys silenced events with it.

@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.
@abnegate

Copy link
Copy Markdown
Member Author

@greptileai review

@abnegate

Copy link
Copy Markdown
Member Author

@greptile-apps review

Force re-review of HEAD 7a3a60e. Description updated for factories and caret lock refresh.

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.
Comment thread src/Migration/Destinations/Appwrite.php
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.
@abnegate

Copy link
Copy Markdown
Member Author

@greptileai review

abnegate and others added 2 commits August 21, 2026 22:53
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>
Comment thread src/Migration/Destinations/Appwrite.php
abnegate and others added 2 commits August 27, 2026 13:59
The Appwrite destination passed the 'ASC'/'DESC' strings a source hands back
straight into Utopia\Database\Index, which takes Order cases and rejects
anything else. The resulting InvalidArgumentException is not a Migration
Exception, so instead of recording a failed index the transfer aborted.

This was live before the lock re-pin and simply could not be seen: the lock held
utopia-php/query at a deleted branch whose Index took plain strings, so CI never
built an index against the contract the released library actually has.
AppwriteIndexLengthsTest covers it -- two of its cases pass ['ASC', 'ASC'] and
went red on the first run against the re-pinned lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Recovery was gated on `failed` alone, but `provisioning` is reachable as a
terminal state on its own. markDatabaseFailed() deliberately swallows its own
error so a secondary failure cannot mask the caller's throw, so a metadata store
that is down for both the reload and the status write leaves the document in
`provisioning` with no backing collection.

Under the default OnDuplicate::Fail that document was unrecoverable: the gate
opened only for `failed`, so every retry fell through to createDocument and hit
"Document already exists", and the backing collection was never created. The
`provisioning` handling further down, in the Skip branch, sat inside the same
gate and so was unreachable in exactly the case it was written for.

Both states mean the same thing -- a prior run created the metadata and did not
finish -- so the predicate now covers both. The collection is still only
recreated when it is actually missing, which was already the guard.

testProvisioningDatabaseRetrySucceedsUnderOnDuplicateFail drives the real
writer rather than seeding a status: it fails the reload and then the status
write, asserts the document is stranded in `provisioning` with no collection,
and then asserts the retry recovers it. Seen red as "Document already exists".

Found by Greptile on #222.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abnegate

Copy link
Copy Markdown
Member Author

Recording the reasoning for the P1 ("Provisioning state blocks retries"), since the thread anchor moved when I pushed the fix. It was valid, and is fixed in ff6f1d5.

Confirmed by driving the real writer rather than seeding a status: fail the reload, then fail the status write that would have recorded the failure. markDatabaseFailed() swallows its own error by design, so it cannot mask the caller's throw — which leaves the document in provisioning with no backing collection. The retry then failed with exactly the predicted collision, Document already exists, and never created the collection.

The gate opened only for failed. The provisioning handling further down, in the Skip branch, sat inside that same gate, so it was unreachable in precisely the case it was written for. Both states mean the same thing — a prior run created the metadata and did not finish — so the predicate now covers both. Collection recreation is still guarded on the collection actually being missing, which was already the case.

testProvisioningDatabaseRetrySucceedsUnderOnDuplicateFail mirrors the existing failed test and was seen red first. 90/90 unit tests, PHPStan and Pint clean.

For the record: this was pre-existing on main and untouched by this PR's diff — it surfaced because the review covered the whole file, not just the changed lines. Worth having caught.

Picks up the resend and cloudflare OAuth2 providers in the migration allow-list.

appwrite/appwrite main added both providers, and without the matching entries a
migration of every OAuth2 provider fails the run:

    "No migration resource for OAuth2 provider 'cloudflare'; skipped."
    "No migration resource for OAuth2 provider 'resend'; skipped."

which reddened MigrationsConsoleClientTest on all five engine lanes of
appwrite/appwrite#11649.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant