payloadcms / payloadcms/payload
Copy Locale Data breaks documents on Postgres when drafts are enabled and localized array/blocks rows exist (duplicate key pkey on the row id)
@r1tsuu is already working on this.
Since Sep 11, 2026.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
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 nestedarray
field. - 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 the
draft: 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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.