Copy Locale Data breaks documents on Postgres when drafts are enabled and localized array/blocks rows exist (duplicate key pkey on the row id)
Describe the Bug
On Postgres with versions.drafts enabled, clicking Copy Locale Data on a document whose
localized array/blocks fields already have rows in the source locale fails with
ERROR: duplicate key value violates unique constraint "<table>_pkey"
DETAIL: Key (id)=(<the SOURCE locale's live row id>) already exists.and the admin surfaces the misleading field error "The following field is invalid: id". After the failed attempt, subsequent saves of the document can keep failing the same way (the stale ids stay in the open form), effectively bricking the document for editors.
We measured the full chain on 3.88.0 (each link verified in isolation, probes below):
- The target-locale fetch falls back to the source locale.
copyDataFromLocalefetches the target locale withoutfallbackLocale: false(packages/ui/src/utilities/copyDataFromLocale.ts— thepayload.findByID({ locale: toLocale, draft: true, … })call). With an empty target locale, Payload's locale fallback returns the source locale's data — including all array/blocks rowids and_status: 'published'. As a side effect, the "target is empty" merge branch can never fire for documents that have source data. removeIdIfParentIsLocalizedthen strips all row ids below localized fields, and the handler callspayload.update({ data, draft: true, locale: toLocale })— with the merged data still carrying_status: 'published'from step 1.draft: truecombined with incoming_status: 'published'rewrites the LIVE rows of BOTH locales — and the write re-inserts the source locale's rows without deleting them first. Because the generated row tables use a single-columnidprimary key (not composite(id, _locale)— same anatomy as #16054), the second insert collides with the source locale's still-live row id.
Isolation probes (all on 3.88.0, Postgres 16, drafts + autosave enabled)
| Probe | Payload of payload.update({ draft: true, locale: 'en' }) |
Result |
|---|---|---|
| D1 | rows without ids, _status: 'draft' |
✅ succeeds |
| D2 | rows with fresh ids, _status: 'draft' |
✅ succeeds |
| D3 | rows without ids, _status: 'published' |
❌ pkey violation (2-row INSERT re-inserts the DE live row) |
| D4 | rows with fresh ids, _status: 'published' |
❌ pkey violation — fresh ids do not help; the status combination is the trigger |
So the failure is not the id copying per se: it is the draft: true update whose data still
carries _status: 'published', fed by the fallback fetch. Collection-level hooks cannot heal
it — the colliding row comes from the internal live-row rewrite, not from data.
Why the existing tests don't catch it
The copy-locale integration tests run against fixtures with versions: false — the
drafts + localized-rows + Postgres combination is untested.
To Reproduce
- Postgres adapter, a collection with
versions: { drafts: { autosave: true } }, two locales (dedefault,en), and a localizedblocksfield containing a block with a nestedarrayfield. - Create a document in
de, add a block with one array row, publish it. - Switch the admin to
en(empty locale) and use Copy Locale Data (de→en). - The update fails with the pkey violation above; the admin shows "The following field is invalid: id".
Suggested fix (happy to open a PR)
Two independent, small changes in copyDataFromLocale:
- Fetch both locales with
fallbackLocale: false— the target fetch must reflect what the target locale actually contains, otherwise the empty-target merge branch is dead code and the source's_statusleaks into the update. - Strip
_status(andcreatedAt/updatedAt) from the merged data before thedraft: trueupdate — copying a locale should produce a draft of the target locale, never an implicit publish that rewrites both locales' live rows.
We have a fork/branch prepared (fix/copy-locale-fresh-row-ids-3x off 3.x) and can turn this
into a PR with an integration test (drafts-enabled fixture) if the direction is agreed.
Environment
- Payload: 3.88.0 (
@payloadcms/db-postgres,@payloadcms/richtext-lexical) - Database: PostgreSQL 16
- Next.js 16.2.12, Node 24.x
- Related: #16054 (single-column pkey on localized array rows — same underlying anatomy)
Source: payloadcms/payload