Repository navigation
bulkOperationsSingleTransaction: bulk update and delete return documents that were rolled back #18494
Description
Describe the Bug
bulkOperationsSingleTransaction is documented as: "When true, bulk update and delete operations will process documents one at a time in separate transactions instead of all at once in a single transaction." The DocumentDB and Cosmos DB compatibility options turn it on by default.
With it on, a failing document rolls back the documents processed before it, but the operation still returns them in docs as updated or deleted. Callers, including the REST bulk endpoints the admin list view uses, are told a write happened when it didn't.
In the reproduction, hooks refuse to update or delete the post titled fail. The posts are a, b, fail, c, d:
| Operation | Returned in docs |
Actually in the database |
|---|---|---|
bulk update (sorted by createdAt) |
a, b, c, d updated |
only c, d updated |
| bulk delete | a, b, c, d deleted |
all five posts still there |
Expected: every document has its own transaction, so a, b, c, d are updated or deleted, and only fail is reported in errors.
Where it happens in payload@3.90.2 (dist/collections/operations):
update.js:37anddelete.js:28start one transaction for the whole operation. The per-documentinitTransaction(req)(update.js:206,delete.js:92) then returnsfalse, becausereqalready has that transaction, so no document gets its own.- A failing document's
catchcallskillTransaction(req)(update.js:291,delete.js:225). That rolls back the shared transaction, including the documents already returned indocs, and removes it fromreq, so the documents after it start their own transactions. - For delete,
docs.map(async ...)atdelete.js:85starts every document at once, so the loop atdelete.js:238-242doesn't run them one at a time. That part is also reported in payload.delete({ where }) silently fails to delete records — concurrent deleteOne race condition (regression since 3.41.0) #16075. fix: prevent bulk operation data loss and surface upload failures #18266 makes bulk operations sequential but keeps the operation-level transaction, so the rollback above still happens with it.
In 4.0.0-canary.37, delete.js:29 already skips the operation-level transaction when the option is on and processes documents one by one. update.js:38 still starts it, so bulk update has the same problem on v4.
Skipping the operation-level initTransaction when bulkOperationsSingleTransaction is on, in both update.ts and delete.ts (as canary's delete does), and running the delete documents one after another fixes it: with 3.90.2's dist patched that way, both tests in the reproduction pass.
Link to the code that reproduces this issue
https://github.com/Livog/payload-bulk-operations-single-transaction-repro
Reproduction Steps
- Clone the reproduction (created with
create-payload-app -t blank, MongoDB). The second commit holds all changes from the template. pnpm installpnpm test:int
No database setup is needed: the test starts a single-node MongoDB 7.0.24 replica set with mongodb-memory-server.
Both tests fail, and the console shows what Payload returned next to what is in the database:
bulk update {
reportedUpdated: [ 'a', 'b', 'c', 'd' ],
reportedErrors: [ 'This post cannot be updated' ],
actuallyArchived: [ 'c', 'd' ],
notArchived: [ 'a', 'b', 'fail' ]
}
bulk delete {
reportedDeleted: [ 'a', 'b', 'c', 'd' ],
reportedErrors: [ 'This post cannot be deleted' ],
stillInDatabase: [ 'a', 'b', 'c', 'd', 'fail' ]
}
Files:
src/payload.config.ts:mongooseAdapter({ bulkOperationsSingleTransaction: true })src/collections/Posts.ts: hooks that refuse to update or delete the post titledfail. The delete hook waits one second first, standing in for async work, so the other posts are processed before it refuses.tests/int/bulk-operations-single-transaction.int.spec.ts: seeds the posts, runs the bulk update and the bulk delete, and compares the result with the database.
Which area(s) are affected?
area: core, db: mongodb
Environment Info
Binaries:
Node: 22.14.0
npm: 10.9.2
Yarn: 1.22.22
pnpm: 10.34.6
Relevant Packages:
payload: 3.90.2
next: 16.3.3
@payloadcms/db-mongodb: 3.90.2
@payloadcms/graphql: 3.90.2
@payloadcms/next/utilities: 3.90.2
@payloadcms/richtext-lexical: 3.90.2
@payloadcms/translations: 3.90.2
@payloadcms/ui/shared: 3.90.2
react: 19.2.6
react-dom: 19.2.6
Operating System:
Platform: darwin
Arch: arm64
MongoDB: 7.0.24, single-node replica set (mongodb-memory-server 10.4.3)
Activity
fix: skip operation transaction in bulk update when bulkOperationsSin…
test(database): add test for partial bulk update failure with bulkOpe…