Skip to content

feat: add onDuplicate option to DestinationAppwrite#169

Merged
abnegate merged 7 commits intomainfrom
feat/skip-duplicates
Apr 20, 2026
Merged

feat: add onDuplicate option to DestinationAppwrite#169
abnegate merged 7 commits intomainfrom
feat/skip-duplicates

Conversation

@premtsd-code
Copy link
Copy Markdown
Contributor

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

Summary

Adds an $onDuplicate constructor param to DestinationAppwrite so CSV / JSON / appwrite→appwrite imports can control how duplicate-id rows are handled. Three modes:

  • onDuplicate = "fail" (default) — plain createDocuments(). Fails fast on DuplicateException. Original behavior, unchanged.
  • onDuplicate = "skip" — wraps createDocuments() in skipDuplicates() scope guard. Silently no-ops duplicate ids at the adapter layer (INSERT IGNORE equivalent). Existing rows preserved unchanged.
  • onDuplicate = "upsert"upsertDocuments() instead of createDocuments(). Replaces existing rows with imported values.

The existing skipRelationshipsExistCheck() FK-guard is preserved in all three branches.

Design

Single string param + class constants + WhiteList validator at the REST boundary. This matches:

  • Appwrite REST conventionsencryption, priority, period, grant_type etc. across app/controllers/api/*.php all use new WhiteList(['a', 'b', 'c']) over raw strings. None of the 92 enum-style WhiteList params in Appwrite use PHP backed enums.
  • migration package conventions — class-constant pattern already used here (Resource::STATUS_*, Resource::TYPE_*).

Defines ON_DUPLICATE_FAIL, ON_DUPLICATE_SKIP, ON_DUPLICATE_UPSERT class constants on Destinations\Appwrite, plus an ON_DUPLICATES array consumers can drop straight into WhiteList(...). Invalid values throw InvalidArgumentException in the constructor.

Changes

  • src/Migration/Destinations/Appwrite.php — constructor accepts string $onDuplicate = self::ON_DUPLICATE_FAIL; createRecord() row-buffer flush dispatches via match on the three modes.

Dependency

Uses skipDuplicates() scope guard from utopia-php/database@5.3.22 (already tagged, includes utopia-php/database#852). composer.json keeps the existing "utopia-php/database": "5.*" wildcard; composer.lock resolves to 5.3.22.

Test plan

  • Static analysis (PHPStan level 3) clean
  • Pint / PSR-12 format clean
  • 15/15 unit tests pass
  • E2E coverage in appwrite/appwrite#11910: testCreateCSVImportSkipDuplicates, testCreateCSVImportOverwrite, testCreateCSVImportDefaultFailsOnDuplicate — all passing locally against a full appwrite stack

Downstream PR

CSV / JSON / appwrite-to-appwrite imports that re-run on the same
batch (e.g. user re-uploads the same file, or a worker retries a
failed chunk) currently throw DuplicateException and abort the whole
batch. Wrap the row-buffer flush in the new skipDuplicates() scope
guard so duplicate-by-id rows are silently no-op'd at the adapter
layer (INSERT IGNORE / ON CONFLICT DO NOTHING / $setOnInsert), letting
the rest of the batch proceed.

The existing skipRelationshipsExistCheck() wrapper is preserved
(FK-target guard); skipDuplicates composes around it.

Feature-branch note: depends on utopia-php/database's skipDuplicates()
scope guard from PR utopia-php/database#852. composer.json is
temporarily pinned to dev-csv-import-upsert-v2 with a 5.99.0 alias so
composer can resolve the 5.* constraint transitively. Must be reset
to the proper release version (^5.X.Y) once PR #852 merges and
utopia-php/database ships.
Per Jake's spec, migration destinations accept two new behavior flags:

- overwrite=true → use upsertDocuments() instead of createDocuments()
  Replaces existing rows with the imported values. Naturally handles
  duplicate ids.

- skip=true → wrap createDocuments() in skipDuplicates() scope guard.
  Silently no-ops duplicate ids at the adapter layer (INSERT IGNORE
  equivalent). Existing rows are preserved.

Default (both false): plain createDocuments, fails fast on
DuplicateException. Original behavior, unchanged for existing callers.

Precedence when both set: overwrite wins (upsert subsumes skip).

The existing skipRelationshipsExistCheck() FK-guard wrapper is
preserved in all three branches.
@premtsd-code premtsd-code marked this pull request as ready for review April 20, 2026 09:47
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR introduces a PHP backed enum OnDuplicate and wires it into DestinationAppwrite's document-flush path so callers can choose fail (default, unchanged), skip (INSERT IGNORE semantics via skipDuplicates()), or upsert (upsertDocuments()) when importing rows with existing IDs. The constructor change is fully backward-compatible (new optional trailing parameter), the match is exhaustive over all three enum cases, and the skipRelationshipsExistCheck guard is preserved in every arm.

Confidence Score: 5/5

Safe to merge — no blocking issues found; default behavior is unchanged and all three dispatch arms are correct.

All three match arms preserve the existing skipRelationshipsExistCheck guard, the new parameter is optional with the original behaviour as default, the enum is exhaustive so no UnhandledMatchError risk, and the composer.lock correctly resolves to the tagged 5.3.22 release that includes skipDuplicates().

No files require special attention.

Important Files Changed

Filename Overview
src/Migration/Destinations/OnDuplicate.php New backed enum with Fail/Skip/Upsert cases and a values() helper for REST-layer WhiteList construction — clean, no issues.
src/Migration/Destinations/Appwrite.php Constructor gains optional OnDuplicate $onDuplicate = OnDuplicate::Fail; flush path switched to an exhaustive match dispatching upsertDocuments, skipDuplicates+createDocuments, or plain createDocuments — all three arms preserve the existing skipRelationshipsExistCheck guard.
composer.lock Bumps utopia-php/database from 5.2.1 → 5.3.22 (tagged release); transitive dependency shuffle replaces utopia-php/compression/utopia-php/http/utopia-php/framework with utopia-php/console + utopia-php/validators; Composer plugin-api version bumped 2.6.0 → 2.9.0.

Reviews (3): Last reviewed commit: "Move OnDuplicate enum to Destinations na..." | Re-trigger Greptile

Comment thread composer.lock
@@ -2246,9 +2248,10 @@
"ext-pdo": "*",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 skipDuplicates() availability in 5.3.22 needs verification

The PR description states the lock must be pinned to the unreleased dev-csv-import-upsert-v2 branch because skipDuplicates() doesn't exist in any released version of utopia-php/database. However, the actual composer.lock here resolves to the tagged release 5.3.22 — not a dev branch. If skipDuplicates() is absent from 5.3.22, the ON_DUPLICATE_SKIP code path will throw a fatal "Call to undefined method" error at runtime even though static analysis may have passed (PHPStan level 3 may not resolve the method through the scope guard pattern). Please confirm whether 5.3.22 includes skipDuplicates() and update the PR description / draft status accordingly.

Comment thread src/Migration/Destinations/Appwrite.php
@premtsd-code premtsd-code changed the title feat: add overwrite and skip options to DestinationAppwrite feat: add onDuplicate option to DestinationAppwrite Apr 20, 2026
Comment thread src/Migration/Destinations/Appwrite.php Outdated
Comment on lines +66 to +77
public const ON_DUPLICATE_FAIL = 'fail';
public const ON_DUPLICATE_SKIP = 'skip';
public const ON_DUPLICATE_UPSERT = 'upsert';

/**
* @var array<string>
*/
public const ON_DUPLICATES = [
self::ON_DUPLICATE_FAIL,
self::ON_DUPLICATE_SKIP,
self::ON_DUPLICATE_UPSERT,
];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use an enum, push for modern PHP features in new code

@abnegate abnegate merged commit 9726690 into main Apr 20, 2026
4 checks passed
@abnegate abnegate deleted the feat/skip-duplicates branch April 20, 2026 11:55
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