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

@premtsd-codepremtsd-code commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

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

ValueBehavior
"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

github-actionsBot commented Apr 15, 2026

Copy link
Copy Markdown

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit e6a9c68 - 26 flaky tests
TestRetriesTotal TimeDetails
WebhooksCustomServerTest::testDeleteDeployment126msLogs
WebhooksCustomServerTest::testDeleteFunction112msLogs
WebhooksCustomServerTest::testCreateCollection111msLogs
WebhooksCustomServerTest::testCreateAttributes121msLogs
WebhooksCustomServerTest::testCreateDocument117msLogs
WebhooksCustomServerTest::testUpdateDocument1123msLogs
WebhooksCustomServerTest::testDeleteDocument113msLogs
WebhooksCustomServerTest::testCreateTable121msLogs
WebhooksCustomServerTest::testCreateColumns130msLogs
WebhooksCustomServerTest::testCreateRow129msLogs
WebhooksCustomServerTest::testUpdateRow117msLogs
WebhooksCustomServerTest::testDeleteRow123msLogs
WebhooksCustomServerTest::testCreateStorageBucket115msLogs
WebhooksCustomServerTest::testUpdateStorageBucket114msLogs
WebhooksCustomServerTest::testCreateBucketFile112msLogs
WebhooksCustomServerTest::testUpdateBucketFile16msLogs
WebhooksCustomServerTest::testDeleteBucketFile16msLogs
WebhooksCustomServerTest::testDeleteStorageBucket115msLogs
WebhooksCustomServerTest::testCreateTeam17msLogs
WebhooksCustomServerTest::testUpdateTeam112msLogs
WebhooksCustomServerTest::testUpdateTeamPrefs116msLogs
WebhooksCustomServerTest::testDeleteTeam16msLogs
WebhooksCustomServerTest::testCreateTeamMembership116msLogs
WebhooksCustomServerTest::testDeleteTeamMembership111msLogs
WebhooksCustomServerTest::testWebhookAutoDisable131msLogs
UsageTest::testPrepareDatabaseStatsTablesAPI132.30sLogs
Commit e63f9fd - 4 flaky tests
TestRetriesTotal TimeDetails
UsageTest::testFunctionsStats110.15sLogs
UsageTest::testPrepareSitesStats16msLogs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage14msLogs
WebhooksCustomServerTest::testExecutions12.88sLogs
Commit f9c5f41 - 5 flaky tests
TestRetriesTotal TimeDetails
UsageTest::testFunctionsStats110.17sLogs
UsageTest::testPrepareSitesStats17msLogs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage14msLogs
DatabasesStringTypesTest::testCreateTable1242.15sLogs
TablesDBTransactionsCustomServerTest::testMixedSingleOperations1240.53sLogs
Commit 2e9841c - 28 flaky tests
TestRetriesTotal TimeDetails
UsageTest::testFunctionsStats110.20sLogs
UsageTest::testPrepareSitesStats16msLogs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage15msLogs
WebhooksCustomServerTest::testDeleteDeployment114msLogs
WebhooksCustomServerTest::testDeleteFunction110msLogs
WebhooksCustomServerTest::testCreateCollection113msLogs
WebhooksCustomServerTest::testCreateAttributes145msLogs
WebhooksCustomServerTest::testCreateDocument117msLogs
WebhooksCustomServerTest::testUpdateDocument123msLogs
WebhooksCustomServerTest::testDeleteDocument1119msLogs
WebhooksCustomServerTest::testCreateTable111msLogs
WebhooksCustomServerTest::testCreateColumns19msLogs
WebhooksCustomServerTest::testCreateRow112msLogs
WebhooksCustomServerTest::testUpdateRow113msLogs
WebhooksCustomServerTest::testDeleteRow117msLogs
WebhooksCustomServerTest::testCreateStorageBucket17msLogs
WebhooksCustomServerTest::testUpdateStorageBucket112msLogs
WebhooksCustomServerTest::testCreateBucketFile111msLogs
WebhooksCustomServerTest::testUpdateBucketFile17msLogs
WebhooksCustomServerTest::testDeleteBucketFile18msLogs
WebhooksCustomServerTest::testDeleteStorageBucket117msLogs
WebhooksCustomServerTest::testCreateTeam16msLogs
WebhooksCustomServerTest::testUpdateTeam111msLogs
WebhooksCustomServerTest::testUpdateTeamPrefs113msLogs
WebhooksCustomServerTest::testDeleteTeam17msLogs
WebhooksCustomServerTest::testCreateTeamMembership120msLogs
WebhooksCustomServerTest::testDeleteTeamMembership111msLogs
WebhooksCustomServerTest::testWebhookAutoDisable132msLogs
Commit e621701 - 5 flaky tests
TestRetriesTotal TimeDetails
VectorsDBConsoleClientTest::testGetCollectionLogs16msLogs
DatabasesConsoleClientTest::testGetCollectionLogs16msLogs
UsageTest::testFunctionsStats110.17sLogs
UsageTest::testPrepareSitesStats17msLogs
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage15msLogs

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

@github-actions

github-actionsBot commented Apr 15, 2026

Copy link
Copy Markdown

✨ Benchmark results

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

Before

ScenarioP50 (ms)P95 (ms)RequestsRPS
API total15.72140.9518532.74
Account25.77180.22356.79
TablesDB14.6725.19358.1
Storage13.3757.27517
Functions23.8636.06409.23

After

ScenarioP50 (ms)P95 (ms)RequestsRPS
API total15.47138.1618533.45
Account25.22160.99356.89
TablesDB14.1425.88358.23
Storage12.5257.197517.35
Functions21.7336.08409.38

Delta

ScenarioP95 delta (ms)
API total-2.79
Account-19.23
TablesDB+0.69
Storage-0.01
Functions+0.02
Top API waits
API requestMax wait (ms)
account.logs.list545.54
account.password.update160.76
account.sessions.email.create145.62

@blacksmith-sh

This comment has been minimized.

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

greptile-appsBot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

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

FilenameOverview
src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.phpAdds 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.phpAdds 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.phpMirrors the CSV controller: adds onDuplicate param with correct validation and persists it in options; implementation is consistent.
src/Appwrite/Platform/Workers/Migrations.phpPasses 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.phpAdds 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 threadcomposer.json Outdated
Comment threadtests/e2e/Services/Migrations/MigrationsBase.php
@blacksmith-sh

This comment has been minimized.

Comment thread.github/workflows/ci.yml Outdated
…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 threadcomposer.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 threadtests/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 threadsrc/Appwrite/Platform/Workers/Migrations.php
Comment threadtests/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 threadcomposer.json Outdated
Comment threadtests/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.xMay 7, 2026
77 of 80 checks passed
@abnegate
abnegate deleted the feat/skip-duplicates branch May 7, 2026 07:21
Sign up for freeto 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

@premtsd-code@abnegate