createVersion / createGlobalVersion rewrite every older version's latest flag on each save
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 84/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- nodejs, postgresql, typescript
- Domain
- backend, database, performance
Research direction
Start with packages/drizzle/src/createVersion.ts and packages/drizzle/src/createGlobalVersion.ts, then inspect the shared database tests for version saves across Postgres, SQLite, and D1. Reproduce a document or global version save with many older versions and verify that only the currently latest row is rewritten while the final latest state remains unchanged.
Written by the indexing model from the issue text.
Description
Describe the Bug
On every save of a document with drafts, @payloadcms/drizzle's createVersion sets latest = false on every older version of that document, not just the one that is currently latest:
UPDATE <versions>
SET latest = false
WHERE id != $new
AND parent_id = $parent
AND updated_at < $now
Only one older row is ever latest = true. All the others are already false, but they are rewritten anyway. Because latest is indexed, each rewrite is a full (non-HOT) row update.
On Postgres, for a document with 100 versions (maxPerDoc: 100), we measured:
- Today: 100 rows updated per save, about 1,450 buffers, 9–10 ms.
- With
AND latest IS NOT FALSEadded: 1 row updated, 0.4 ms.
The final state is the same, because rows that are already false stay false.
createGlobalVersion has the same pattern with no other condition (WHERE id != $new), so a global rewrites every version row on each save.
Possibly related: #17216 (duplicate latest rows under autosave on D1/SQLite). That issue is about correctness; this one is about cost. Narrowing the UPDATE to the row that is actually latest would also make the statement much smaller for that case.
Link to the code that causes the issue
packages/drizzle/src/createVersion.ts and packages/drizzle/src/createGlobalVersion.ts (seen in 3.89.0, still present in 3.90.2).
Reproduction Steps
- Create a collection with
versions: { drafts: true, maxPerDoc: 100 }on@payloadcms/db-postgres. - Save one document 100 times.
- Save it again with Postgres statement logging, or
EXPLAIN (ANALYZE, BUFFERS)the UPDATE above. It updates about 100 rows, where 1 would do.
Suggested fix, which we run as a local patch:
WHERE ${table.id} != ${result.id}
AND ${table.parent} = ${parent}
AND ${table.updatedAt} < ${result.updatedAt || updatedAt}
+ AND ${table.latest} IS NOT FALSE
The same condition would apply to createGlobalVersion.
Which area(s) are affected?
db: postgres, db: drizzle (also sqlite and d1, which share the code)
Environment Info
payload: 3.89.0 (code unchanged in 3.90.2)
@payloadcms/db-postgres: 3.89.0
Database: PostgreSQL 16 (AWS RDS)
Node: 22
- Dominant language
- TypeScript
- Stars
- 45.5k
- Forks
- 4.3k
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 312
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing 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.
More from payloadcms/payload
-
WhereBuilder throws "Cannot read properties of undefined (reading 'value')" when the list URL filters on a field that is not filterablePossibly taken A pull request linked to this issue is open or already merged. OpenBug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
payloadcms/payload#19002 · 1 comment ·
Maintainers usually reply within 1 day
-
Regression: `findByID` with `disableErrors: true` throws `NotFound` for null/empty ids since 3.91.0Possibly taken @Krrishkumar125 claimed this 7 days ago. Openarea: core Bug invalid-reproduction status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
payloadcms/payload#18935 · 2 comments ·
Maintainers usually reply within 1 day
-
bug: Local API call with req resets req.fallbackLocale to defaultLocalePossibly taken A pull request linked to this issue is open or already merged. Openarea: core Bug created-by: Contributor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
payloadcms/payload#18768 ·
Maintainers usually reply within 1 day
-
[plugin-multi-tenant] Infinite redirect loop with custom tenantFieldName and isGlobal collectionsPossibly taken A pull request linked to this issue is open or already merged. OpenBug plugin: multi-tenant status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
payloadcms/payload#18690 ·
Maintainers usually reply within 1 day
-
Global update drops overrideAccess, so Local API updateGlobal fails filterOptions validation (3.90 regression)Possibly taken @PINYOPATTANAWASANPORN claimed this 15 days ago. Openarea: core Bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
payloadcms/payload#18674 · 2 comments · 1 reaction ·
Maintainers usually reply within 1 day
All issues in payloadcms/payload
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
liberation-data/drivine#428 · 1 comment ·
Maintainers usually reply within 1 day
-
GitLab: a merge request closed without merging keeps the Git toolbar on View MR instead of Create MROpen
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
code-yeongyu/oh-my-openagent#11731 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100