server-ce 1.9.x's tablesdb POST /rows tightened input validation: the
modular Documents/Create.php rejects `data => []` with a 400
"missing data" because TablesDB's Rows/Create.php inherits the strict
default of getSupportForEmptyDocument() = false (only DocumentsDB
overrides it to true). The test was relying on the older permissive
behavior to seed an empty parent row before the relationship cascade
links it.
Add a non-relationship `label` string column on the parents table and
populate it with `data => ['label' => 'p1']` so the POST passes the
empty-data guard. The test's actual assertion target — partner-side
pair-key dedup on DropAndRecreate — is unchanged.
Cascade fixes: testAppwriteMigrationOverwriteAttributeRecreate and
testAppwriteMigrationOverwriteSameSpecRecreate were failing in the
retry pass because TwoWayRecreate's bail at L1616 left source/dest
state uncleaned. Once TwoWayRecreate completes, those tests see a
clean project again.
Caught in CI run 25419479164 / job 74562934987 on the MongoDB
(dedicated) Migrations matrix.
Maintainer review on utopia-php/migration#171 renamed
OnDuplicate::Upsert -> OnDuplicate::Overwrite (value 'upsert' ->
'overwrite') to align with Appwrite terms (skip / overwrite / fail).
Applying the cross-repo ripple here:
- app/controllers/api/migrations.php: 3 endpoint param descriptions
updated ('upsert' -> 'overwrite' in the help text). The validator
still uses OnDuplicate::values() so it auto-picks up the new value.
- tests/e2e/Services/Migrations/MigrationsBase.php: all
'onDuplicate' => 'upsert' -> 'overwrite'; method names
testAppwriteMigrationUpsert* -> testAppwriteMigrationOverwrite*;
comments / assertion messages / local var names switched.
- Left untouched: utopia's upsertDocuments operation, transaction
TransactionState 'upsert' action, Operation validator — those refer
to the database-level upsert primitive, not the OnDuplicate enum.
composer.lock: utopia-php/migration 7d71505 -> b8ae7bc.
After dropping createdAt from resolveSchemaAction, source-side recreate
no longer routes through DropAndRecreate via the outer decision. The
inner fallthrough still drops+recreates when the spec diff is a non-SDK
change, so this test now toggles 'array' (a non-SDK field) on recreate
to actually exercise the drop+recreate path it pins.
Also clarifies the two-way recreate test's docblock — with createdAt
gone and identical spec on recreate, it exercises spec-match + pair-key
dedup (both tolerate paths) rather than parent-side drop. End-state
assertions unchanged.
testAppwriteMigrationUpsertSameSpecRecreateTolerates exercises the new
spec-match guard added in utopia-php/migration c8d1789. Source drops +
recreates a column with the EXACT same spec as before; createdAt
advances but specs match → action is forced to Tolerate. Asserts dest
column's $createdAt stays at first-migration value (proving Tolerate,
not DropAndRecreate). Row pass under Upsert still propagates source's
new row value.
Companion to testAppwriteMigrationUpsertAttributeRecreateDropsAndRecreates
which exercises the spec-DIFFERS path: same precondition (drop + recreate),
different outcome (DropAndRecreate vs Tolerate) gated on spec equality.
composer.lock: utopia-php/migration 24fd23b -> c8d1789 (spec-match guard).
Two coverage gaps closed:
- testAppwriteMigrationUpsertOneWayRelationshipDropAndRecreate exercises
the path that updateRelationshipInPlace gates off: one-way + onDelete
change → returns false → falls through to DropAndRecreate via
deleteRelationship. Coverage was lost when
testAppwriteMigrationUpsertUpdatesRelationshipOnDeleteInPlace was
converted to two-way to actually hit the in-place path.
- testAppwriteMigrationUpsertAttributeRecreateDropsAndRecreates pins
the createdAt-different leaf path: source drops + recreates the
attribute (createdAt advances), re-migration must DropAndRecreate
on dest and re-flow the row data through the row pass. Companion to
testAppwriteMigrationUpsertUpdatesAttributeInPlace which covers the
same-createdAt + newer-updatedAt path.
Migration package already at 09c1b21 (the maintainability commit) from
the previous lock bump — no further composer.lock change needed.
testAppwriteMigrationUpsertTwoWayRecreateSkipsPartnerSide exercises
the DropAndRecreate path on a two-way relationship that the partner-
side pair-key dedup guards. Source recreates the relationship between
runs, forcing parent-side createdAt diff. Test asserts the migration
completes cleanly and partner-table rows survive — without dedup, the
partner pass re-fires DropAndRecreate and destroys those rows.
composer.lock: utopia-php/migration c76de9a -> c13e77d (partner-side
pair-key dedup restored).
The previous version of this test created a one-way relationship,
which falls through to DropAndRecreate (one-way + onDelete change is
gated off in updateRelationshipInPlace because utopia's
updateRelationship partner-cascade throws on one-way). It never
exercised the in-place path it was named for.
Converted to two-way (parents.kids ↔ children.parent), and asserted
both parent- and partner-side onDelete on dest. Partner-side
assertion is the regression guard for the partner-meta refresh that
was missing from updateRelationshipInPlace.
composer.lock: utopia-php/migration a36d95f -> c76de9a (partner-side
onDelete sync fix).
Three new e2e tests in MigrationsBase covering the schema reconciliation
paths added in utopia-php/migration:
- testAppwriteMigrationUpsertUpdatesAttributeInPlace: PATCH source
required/default (SDK-reachable), assert dest reflects change and the
pre-existing row's column data is preserved (drop+recreate would have
wiped it).
- testAppwriteMigrationSkipPreservesAttributeDrift: leaf-level analog
of the existing container-drift Skip test — guards Skip from ever
consulting timestamps.
- testAppwriteMigrationUpsertUpdatesRelationshipOnDeleteInPlace: PATCH
source onDelete cascade->restrict (SDK-reachable), assert dest
reflects change and structural fields (relationType, twoWay) untouched.
composer.lock: utopia-php/migration 6e6f825 -> a36d95f (mechanical
helpers replacement, parseTimestamp dedup, match dispatch, comment trim).
testAppwriteMigrationUpsertDropsOrphanColumn: adds a column directly on
destination (simulating post-rename orphan or dest-only drift), runs
Upsert, asserts the orphan is dropped and source-declared column
survives. Covers the per-table orphan cleanup fired inside
createRecord before rows land.
testAppwriteMigrationSkipKeepsOrphanColumn: same setup, Skip mode.
Asserts the orphan survives, proving the cleanup is correctly gated
to Upsert only.
Two new E2E tests exercising the schema-tolerance UpdateInPlace path
added in utopia-php/migration's DestinationAppwrite.
testAppwriteMigrationUpsertUpdatesContainerMetadata (positive):
- Fresh migration copies source database + table + column + row to dest.
- Mutates source database name (PUT /databases/:id) and table
name/permissions/rowSecurity/enabled (PUT /tablesdb/:db/tables/:id).
- One-second sleep before mutation ensures source's $updatedAt is
strictly greater than dest's at second granularity (strtotime
comparison).
- Upsert re-migration asserts:
- 'completed' status.
- dest database name matches source's new name.
- dest table name / enabled / rowSecurity match source's new values.
- child row's 'name' attribute is untouched — UpdateInPlace only
rewrites container metadata, not rows.
testAppwriteMigrationSkipPreservesContainerDrift (negative):
- Fresh migration, then mutate BOTH dest (simulating ops tightening
permissions post-migration) and source (divergence).
- Skip re-migration asserts dest kept its tightened values — Skip's
strict "don't touch" contract protects dev→prod cutover workflows
from accidentally wiping ops-side drift on schema re-sync.
Both tests use performMigrationSync for strict 'completed' assertions.
Runtime ~18s combined. Existing testAppwriteMigrationRowsOnDuplicate
and testAppwriteMigrationReRunIsIdempotent regression-tested locally.
utopia-php/migration's DestinationAppwrite now handles schema tolerance on
re-migration (PR #171 on feat/skip-duplicates): it pre-checks destination
`_metadata` for each database / table / column / index and tolerates in
Skip/Upsert mode. Re-runs no longer produce schema-level errors, so the
E2E tests can drop the status-tolerant workaround and assert strict
'completed' outcomes.
Changes:
- composer.json: pin utopia-php/migration to dev-feat/skip-duplicates (aliased
to 1.9.99 for stability resolution). Will be replaced with a fixed 1.10.0
tag once the migration PR lands.
- testAppwriteMigrationRowsOnDuplicate: replace the tolerant
runMigrationAssertingRowSuccess helper with performMigrationSync on the
Skip and Upsert re-runs. Asserts 'completed' status on every run,
destination row content matches the expected value per mode (Mutated
preserved on Skip, Original restored on Upsert). Helper method removed.
- testAppwriteMigrationReRunIsIdempotent (new): seeds two rows on source,
runs the migration three times back-to-back (fresh, Skip re-run, Upsert
re-run) against unchanged source data, asserts strict 'completed' on every
run and row content is stable across all three. Exercises the
schema-tolerance path end-to-end: every database/table/column on
destination already exists with a matching spec, so DestinationAppwrite's
pre-check returns Tolerate for every resource.
Three new test methods in MigrationsBase, following the existing
testCreateCSVImport setup pattern:
- testCreateCSVImportSkipDuplicates
Seeds documents.csv, mutates one row, re-imports with skip=true.
Asserts the mutated row keeps its mutated value (not overwritten
by the CSV's original value) and the row count stays at 100.
- testCreateCSVImportOverwrite
Seeds documents.csv, mutates one row, re-imports with overwrite=true.
Asserts the mutated row is restored to the CSV's original value
(proving upsertDocuments actually replaced the row) and the row
count stays at 100.
- testCreateCSVImportDefaultFailsOnDuplicate
Regression guard: re-imports documents.csv with no flags. Asserts
the migration goes to status=failed with errors populated, proving
the default duplicate-throws behavior is preserved.
All three share a prepareCsvImportFixture() helper that sets up
database + table (name, age columns) + bucket + documents.csv
upload. Returns the known first-row id + original name/age so tests
can mutate and assert on a predictable row.
Reuses the existing documents.csv fixture (100 rows with \$id as the
first column). No new fixture files needed.
- Add getDatabaseResourceType() helper to map database types to resource constants
- Use database-specific resourceType for CSV/JSON import/export instead of hardcoded TYPE_DATABASE
- Skip attribute validation for schemaless databases (DocumentsDB/VectorsDB) in exports
- Parse JSON export queries in migration worker
- Restore MigrationsBase from 1.9.x and append VectorsDB/DocumentsDB E2E tests