Skip to content

feat(migrations): add onDuplicate option to CSV/JSON/Appwrite imports#11910

Merged
abnegate merged 51 commits into
1.9.xfrom
feat/skip-duplicates
May 7, 2026
Merged

feat(migrations): add onDuplicate option to CSV/JSON/Appwrite imports#11910
abnegate merged 51 commits into
1.9.xfrom
feat/skip-duplicates

Conversation

@premtsd-code
Copy link
Copy Markdown
Contributor

@premtsd-code premtsd-code commented Apr 15, 2026

Summary

Adds a single onDuplicate string param to the three migration creation endpoints so CSV / JSON / Appwrite-to-Appwrite imports can choose how to react to any resource (database, table, column, index, row) that already exists at the destination.

POST /v1/migrations/appwrite
POST /v1/migrations/csv/imports
POST /v1/migrations/json/imports

Each accepts optional onDuplicate: "fail" | "skip" | "overwrite" (default "fail").

Parameter semantics

Value Behavior
"fail" (default) Original behavior — destination throws on the first duplicate it sees (databases, tables, columns, indexes, or rows). Re-running an already-migrated source ends in status=failed.
"skip" Destination is preserved as-is. Pre-checks tolerate every duplicate; per-resource drift on the destination (renamed databases, mutated columns, orphan columns/indexes, mutated row data) is left untouched.
"overwrite" Destination is brought back to source. Drift is reconciled: container metadata + attribute SDK fields update in place, non-SDK changes drop+recreate, orphan columns/indexes get cleaned up, rows are replaced. A spec-match guard avoids no-op churn when the source resource was just re-created with an identical spec.

The string is validated by WhiteList(OnDuplicate::values()) at the REST boundary — same convention as other enum-style params (priority, encryption, period, grant_type). Allowed values are owned by Utopia\Migration\Destinations\OnDuplicate.

Scope

onDuplicate drives all resource handling in the destination — not just rows. The migration package's destination resolves a SchemaAction (Create / Skip / Overwrite) per resource based on onDuplicate:

  • Databases: name/enabled drift → restored on overwrite, preserved on skip
  • Tables: same as databases
  • Columns: SDK-mutable fields → UpdateInPlace; non-SDK fields (type/array/format/filters) → drop+recreate; orphans dropped on overwrite, kept on skip
  • Indexes: type/columns drift handled the same way; orphans cleaned on overwrite
  • Relationships: in-place onDelete/onUpdate; structural changes (twoWay, relatedCollection) drop+recreate with partner-side pair-key dedup
  • Rows: standard duplicate handling

On overwrite, a destination-newer guard ($updatedAt > source) tolerates the local copy so a fresh edit on dest isn't clobbered by a stale source.

The param is persisted on the migration document under options.onDuplicate; the worker reads it back via OnDuplicate::tryFrom($options['onDuplicate'] ?? '') ?? OnDuplicate::Fail and threads it into the DestinationAppwrite constructor.

Changes

  • src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php — adds onDuplicate param + persists in options
  • src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php — same wiring
  • src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php — same wiring
  • src/Appwrite/Platform/Workers/Migrations.phpprocessDestination() reads options.onDuplicate and constructs DestinationAppwrite with the resolved enum
  • composer.jsonutopia-php/migration constraint set to 1.* (lock pins to 1.10.0); utopia-php/database is 5.* (lock pins to 5.4.2)
  • tests/e2e/Services/Migrations/MigrationsBase.php — 19 new E2E test methods + fixtures + helpers

E2E test coverage

Container metadata (skip + overwrite, schema-level)

  • testAppwriteMigrationOverwriteUpdatesContainerMetadata — db/table name+enabled drift restored
  • testAppwriteMigrationSkipPreservesContainerDrift — drift preserved

Orphan columns (skip + overwrite)

  • testAppwriteMigrationOverwriteDropsOrphanColumn
  • testAppwriteMigrationSkipKeepsOrphanColumn

Attribute drift (skip + overwrite)

  • testAppwriteMigrationOverwriteUpdatesAttributeInPlace — SDK-mutable field changes propagate via UpdateInPlace
  • testAppwriteMigrationSkipPreservesAttributeDrift — destination drift preserved

Relationship paths (overwrite-driven)

  • testAppwriteMigrationOverwriteUpdatesRelationshipOnDeleteInPlace — onDelete updated in place on both sides
  • testAppwriteMigrationOverwriteOneWayRelationshipDropAndRecreate — structural change forces drop+recreate
  • testAppwriteMigrationOverwriteTwoWayRecreateSkipsPartnerSide — partner-side pair-key dedup pins to existing data
  • testAppwriteMigrationOverwriteAttributeRecreateDropsAndRecreates — non-SDK column change forces drop+recreate
  • testAppwriteMigrationOverwriteSameSpecRecreateTolerates — spec-match guard avoids no-op churn after source-side recreate

Rows

  • testAppwriteMigrationRowsOnDuplicate — fail / skip / overwrite for rows
  • testAppwriteMigrationReRunIsIdempotent — clean re-run with onDuplicate=skip

CSV imports

  • testCreateCSVImportSkipDuplicates
  • testCreateCSVImportOverwrite
  • testCreateCSVImportDefaultFailsOnDuplicate

JSON imports

  • testCreateJSONImportSkipDuplicates
  • testCreateJSONImportOverwrite
  • testCreateJSONImportDefaultFailsOnDuplicate

19 new test methods covering every code path in the destination.

Dependencies

Both upstream deps are tagged releases — no dev-branch pins:

Test plan

  • PHP lint clean
  • Pint / PSR-12 format clean
  • PHPStan level 3 clean
  • composer validate clean
  • CSV + JSON import E2E tests pass
  • Appwrite→Appwrite E2E tests pass in CI (MongoDB dedicated migrations matrix — 55/55, 2750 assertions)
  • Cross-validated end-to-end against a running cloud stack via 36-scenario harness covering schema/rows/relationships/types under all three modes — all green

Related

Exposes two new optional boolean params on the three migration
creation endpoints so CSV / JSON / appwrite-to-appwrite imports can
choose how to handle rows whose IDs already exist at the destination.

Endpoints updated (app/controllers/api/migrations.php):
- POST /v1/migrations/appwrite
- POST /v1/migrations/csv/imports
- POST /v1/migrations/json/imports

Parameter semantics:
- overwrite=true  -> destination uses upsertDocuments instead of
                     createDocuments; existing rows are replaced
                     with imported values
- skip=true       -> destination wraps createDocuments in
                     skipDuplicates; existing rows are preserved
                     unchanged, duplicate-id rows silently no-op
- both false      -> default; fails fast on DuplicateException
                     (original behavior, unchanged)
- both true       -> overwrite wins (upsert subsumes skip)

Both params are stored in the migration Document's options array
(matches the existing pattern for destination behavior config like
path, size, delimiter, bucketId, etc.) and read back in the worker's
processDestination() to instantiate DestinationAppwrite with the
new constructor params.

Feature-branch note: depends on utopia-php/migration#feat/skip-duplicates
(DestinationAppwrite constructor params) which in turn depends on
utopia-php/database#852 (skipDuplicates scope guard). composer.json is
temporarily pinned to dev-feat/skip-duplicates and
dev-csv-import-upsert-v2 respectively; both must be reset to proper
release versions once the upstream PRs merge.
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.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 15, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit e6a9c68 - 26 flaky tests
Test Retries Total Time Details
WebhooksCustomServerTest::testDeleteDeployment 1 26ms Logs
WebhooksCustomServerTest::testDeleteFunction 1 12ms Logs
WebhooksCustomServerTest::testCreateCollection 1 11ms Logs
WebhooksCustomServerTest::testCreateAttributes 1 21ms Logs
WebhooksCustomServerTest::testCreateDocument 1 17ms Logs
WebhooksCustomServerTest::testUpdateDocument 1 123ms Logs
WebhooksCustomServerTest::testDeleteDocument 1 13ms Logs
WebhooksCustomServerTest::testCreateTable 1 21ms Logs
WebhooksCustomServerTest::testCreateColumns 1 30ms Logs
WebhooksCustomServerTest::testCreateRow 1 29ms Logs
WebhooksCustomServerTest::testUpdateRow 1 17ms Logs
WebhooksCustomServerTest::testDeleteRow 1 23ms Logs
WebhooksCustomServerTest::testCreateStorageBucket 1 15ms Logs
WebhooksCustomServerTest::testUpdateStorageBucket 1 14ms Logs
WebhooksCustomServerTest::testCreateBucketFile 1 12ms Logs
WebhooksCustomServerTest::testUpdateBucketFile 1 6ms Logs
WebhooksCustomServerTest::testDeleteBucketFile 1 6ms Logs
WebhooksCustomServerTest::testDeleteStorageBucket 1 15ms Logs
WebhooksCustomServerTest::testCreateTeam 1 7ms Logs
WebhooksCustomServerTest::testUpdateTeam 1 12ms Logs
WebhooksCustomServerTest::testUpdateTeamPrefs 1 16ms Logs
WebhooksCustomServerTest::testDeleteTeam 1 6ms Logs
WebhooksCustomServerTest::testCreateTeamMembership 1 16ms Logs
WebhooksCustomServerTest::testDeleteTeamMembership 1 11ms Logs
WebhooksCustomServerTest::testWebhookAutoDisable 1 31ms Logs
UsageTest::testPrepareDatabaseStatsTablesAPI 1 32.30s Logs
Commit e63f9fd - 4 flaky tests
Test Retries Total Time Details
UsageTest::testFunctionsStats 1 10.15s Logs
UsageTest::testPrepareSitesStats 1 6ms Logs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage 1 4ms Logs
WebhooksCustomServerTest::testExecutions 1 2.88s Logs
Commit f9c5f41 - 5 flaky tests
Test Retries Total Time Details
UsageTest::testFunctionsStats 1 10.17s Logs
UsageTest::testPrepareSitesStats 1 7ms Logs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage 1 4ms Logs
DatabasesStringTypesTest::testCreateTable 1 242.15s Logs
TablesDBTransactionsCustomServerTest::testMixedSingleOperations 1 240.53s Logs
Commit 2e9841c - 28 flaky tests
Test Retries Total Time Details
UsageTest::testFunctionsStats 1 10.20s Logs
UsageTest::testPrepareSitesStats 1 6ms Logs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage 1 5ms Logs
WebhooksCustomServerTest::testDeleteDeployment 1 14ms Logs
WebhooksCustomServerTest::testDeleteFunction 1 10ms Logs
WebhooksCustomServerTest::testCreateCollection 1 13ms Logs
WebhooksCustomServerTest::testCreateAttributes 1 45ms Logs
WebhooksCustomServerTest::testCreateDocument 1 17ms Logs
WebhooksCustomServerTest::testUpdateDocument 1 23ms Logs
WebhooksCustomServerTest::testDeleteDocument 1 119ms Logs
WebhooksCustomServerTest::testCreateTable 1 11ms Logs
WebhooksCustomServerTest::testCreateColumns 1 9ms Logs
WebhooksCustomServerTest::testCreateRow 1 12ms Logs
WebhooksCustomServerTest::testUpdateRow 1 13ms Logs
WebhooksCustomServerTest::testDeleteRow 1 17ms Logs
WebhooksCustomServerTest::testCreateStorageBucket 1 7ms Logs
WebhooksCustomServerTest::testUpdateStorageBucket 1 12ms Logs
WebhooksCustomServerTest::testCreateBucketFile 1 11ms Logs
WebhooksCustomServerTest::testUpdateBucketFile 1 7ms Logs
WebhooksCustomServerTest::testDeleteBucketFile 1 8ms Logs
WebhooksCustomServerTest::testDeleteStorageBucket 1 17ms Logs
WebhooksCustomServerTest::testCreateTeam 1 6ms Logs
WebhooksCustomServerTest::testUpdateTeam 1 11ms Logs
WebhooksCustomServerTest::testUpdateTeamPrefs 1 13ms Logs
WebhooksCustomServerTest::testDeleteTeam 1 7ms Logs
WebhooksCustomServerTest::testCreateTeamMembership 1 20ms Logs
WebhooksCustomServerTest::testDeleteTeamMembership 1 11ms Logs
WebhooksCustomServerTest::testWebhookAutoDisable 1 32ms Logs
Commit e621701 - 5 flaky tests
Test Retries Total Time Details
VectorsDBConsoleClientTest::testGetCollectionLogs 1 6ms Logs
DatabasesConsoleClientTest::testGetCollectionLogs 1 6ms Logs
UsageTest::testFunctionsStats 1 10.17s Logs
UsageTest::testPrepareSitesStats 1 7ms Logs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage 1 5ms Logs

Note: Flaky test results are tracked for the last 5 commits

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 15, 2026

✨ Benchmark results

Comparing 1.9.x (before) to feat/skip-duplicates (after).

Before

Scenario P50 (ms) P95 (ms) Requests RPS
API total 15.72 140.95 185 32.74
Account 25.77 180.22 35 6.79
TablesDB 14.67 25.19 35 8.1
Storage 13.37 57.2 75 17
Functions 23.86 36.06 40 9.23

After

Scenario P50 (ms) P95 (ms) Requests RPS
API total 15.47 138.16 185 33.45
Account 25.22 160.99 35 6.89
TablesDB 14.14 25.88 35 8.23
Storage 12.52 57.19 75 17.35
Functions 21.73 36.08 40 9.38

Delta

Scenario P95 delta (ms)
API total -2.79
Account -19.23
TablesDB +0.69
Storage -0.01
Functions +0.02
Top API waits
API request Max wait (ms)
account.logs.list 545.54
account.password.update 160.76
account.sessions.email.create 145.62

@blacksmith-sh

This comment has been minimized.

@premtsd-code premtsd-code changed the title feat(migrations): add overwrite and skip options to CSV/JSON/Appwrite imports feat(migrations): add onDuplicate option to CSV/JSON/Appwrite imports Apr 20, 2026
@premtsd-code premtsd-code marked this pull request as ready for review April 20, 2026 12:18
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR adds an onDuplicate parameter (fail / skip / overwrite) to the three migration creation endpoints and wires it through to DestinationAppwrite in the worker. The utopia-php/migration and utopia-php/database constraints are updated to wildcard ranges, and 19 new E2E tests are added.

  • All three controllers correctly accept, validate with WhiteList(OnDuplicate::values()), and persist onDuplicate in the migration document's options array.
  • The worker reads $options['onDuplicate'] and passes the resolved OnDuplicate enum to DestinationAppwrite, but the DestinationCSV and DestinationJSON constructors do not receive it — so onDuplicate=skip/overwrite on CSV/JSON import endpoints silently falls back to fail, making those code paths non-functional.
  • Four existing E2E tests (testAppwriteMigrationStorageBucket, testAppwriteMigrationStorageFiles, testAppwriteMigrationFunction, testAppwriteMigrationSite) were removed and not replaced, leaving storage, functions, and sites migration paths without E2E coverage.

Confidence Score: 4/5

The Appwrite-to-Appwrite migration path works correctly, but the CSV and JSON import paths silently ignore the new onDuplicate option — the worker never forwards it to those destination classes.

The worker passes OnDuplicate only to DestinationAppwrite; DestinationCSV and DestinationJSON constructors receive no such argument, so any user calling the CSV or JSON import endpoints with onDuplicate=skip or onDuplicate=overwrite will always get the default fail behaviour regardless of their intent.

src/Appwrite/Platform/Workers/Migrations.php — DestinationCSV and DestinationJSON instantiation needs the OnDuplicate argument added.

Important Files Changed

Filename Overview
src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php Adds onDuplicate optional param with WhiteList(OnDuplicate::values()) validation, stored in the migration document's options array; straightforward and correct.
src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php Adds onDuplicate param alongside path/size in the options array; param acceptance and persistence are correct, consistent with the Appwrite controller.
src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php Mirrors the CSV controller: adds onDuplicate param with correct validation and persists it in options; implementation is consistent.
src/Appwrite/Platform/Workers/Migrations.php Passes OnDuplicate to DestinationAppwrite correctly but DestinationCSV and DestinationJSON constructors receive no OnDuplicate argument, so the feature is silently inert for CSV and JSON imports.
tests/e2e/Services/Migrations/MigrationsBase.php Adds 19 new E2E tests covering onDuplicate behaviour; removes existing storage/function/site migration tests; performCsvMigration and performJsonMigration helpers lack 202 response-code assertions before accessing $body['$id'].

Reviews (45): Last reviewed commit: "Merge branch '1.9.x' into feat/skip-dupl..." | Re-trigger Greptile

Comment thread composer.json Outdated
Comment thread tests/e2e/Services/Migrations/MigrationsBase.php
@premtsd-code premtsd-code force-pushed the feat/skip-duplicates branch from 8a00770 to f3c2502 Compare April 20, 2026 12:58
@blacksmith-sh

This comment has been minimized.

Comment thread .github/workflows/ci.yml Outdated
@premtsd-code premtsd-code force-pushed the feat/skip-duplicates branch from 7057189 to d0603c4 Compare April 20, 2026 15:39
…lerance

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.
Comment thread composer.json Outdated
Branch is iterating on utopia-php/migration's re-migration tolerance.
Other test matrices (unit, general, abuse, screenshots, benchmark, and
every other e2e service) add ~30+ minutes to CI without exercising code
this PR touches. Restrict the matrix to the Migrations service and skip
the unrelated test jobs until the migration work is ready to merge.

All jobs marked with 'TEMP:' comments + 'if: false' — revert to the
full matrix before merging to main.

Static analysis (lint, phpstan, composer audit, specs, locale, security)
still runs on every PR push.
@blacksmith-sh

This comment has been minimized.

Picks up the PR #171 refactor + unit tests:
- resolveSchemaAction decision point consolidation
- deleteAttributeCompletely primitive (two-way cleanup in one place)
- Hardened sourceIsNewer against MySQL zero-date sentinel
- 14 unit tests locking the decision matrix
@blacksmith-sh

This comment has been minimized.

Picks up the UpdateInPlace branch — database/table metadata drift is
now reconciled on Upsert-newer (renames, enable toggles, table
permissions / documentSecurity) via updateDocument, without touching
child rows.
… dest drift

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.
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.
Comment thread tests/e2e/Services/Migrations/MigrationsBase.php
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).
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.
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.
# Conflicts:
#	app/controllers/api/migrations.php
#	composer.lock
Comment thread src/Appwrite/Platform/Workers/Migrations.php
Comment thread tests/e2e/Services/Migrations/MigrationsBase.php
1.9.x reorganized app/controllers/api/migrations.php into the
Platform/Modules/Migrations structure but dropped the onDuplicate
param. After the merge, every e2e migration test that passed
'onDuplicate' => 'overwrite' would 400 since the param wasn't in the
allowlist anymore.

Restoring it on the three endpoints that take row-level conflict
behavior: Appwrite/Create, CSV/Imports/Create, JSON/Imports/Create.
Each:
  - Imports OnDuplicate + WhiteList.
  - Adds optional ->param('onDuplicate', OnDuplicate::Fail->value,
    new WhiteList(OnDuplicate::values()), …).
  - Threads $onDuplicate through the action signature.
  - Stores it on the migration document's 'options' attribute so
    Workers/Migrations.php can pick it up via
    OnDuplicate::tryFrom($options['onDuplicate'] ?? '')
    ?? OnDuplicate::Fail.

Worker code already reads options['onDuplicate'] (unchanged) — no
edits needed there.
Comment thread composer.json Outdated
Comment thread tests/e2e/Services/Migrations/MigrationsBase.php
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.
@abnegate abnegate merged commit 3b2a01f into 1.9.x May 7, 2026
77 of 80 checks passed
@abnegate abnegate deleted the feat/skip-duplicates branch May 7, 2026 07:21
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.

2 participants