Skip to content

fix: roll back bulk delete/update transactions on partial failure instead of committing, fix bulk operation errors not being logged#15517

Open
AlessioGr wants to merge 19 commits intomainfrom
fix/swallowed-error
Open

fix: roll back bulk delete/update transactions on partial failure instead of committing, fix bulk operation errors not being logged#15517
AlessioGr wants to merge 19 commits intomainfrom
fix/swallowed-error

Conversation

@AlessioGr
Copy link
Copy Markdown
Member

@AlessioGr AlessioGr commented Feb 4, 2026

Bulk delete and update operations had inconsistent behavior when documents failed during processing, depending on where the error originated.

The problem: inconsistent partial success

When a document failed due to a hook error (e.g., beforeChange throwing), the error was caught and the transaction was still committed. Other documents in the batch were persisted, and the operation returned partial success (docs containing successful ones, errors containing failed ones). This behavior could be desirable in some cases.

However, when a document failed due to a database constraint (e.g., FK violation), PostgreSQL internally aborts the entire transaction. The error was caught but never logged, and the code continued trying to process remaining documents on the now-dead transaction. Subsequent operations would fail with:

error: current transaction is aborted, commands ignored until end of transaction block

An example of that can be seen here: #14576

This made partial success unreliable - it only worked for hook errors, not database errors. You couldn't depend on it because whether it worked depended on the error type, and when it didn't work, you'd get confusing errors pointing to unrelated operations instead of the root cause.

The fix

Errors are now logged immediately when caught. If any document fails for any reason, the transaction is explicitly rolled back and all documents are reported as failed. This provides consistent, predictable behavior regardless of error type.

Behavioral change

Previously, bulk operations could return partial success when errors occurred in hooks. Now, when transactions are enabled: if any document fails, the entire transaction is rolled back and all documents are reported in the errors array. The docs array will be empty.

This is the only way for bulk operations to work reliably with transactions. When multiple documents share a transaction, any database-level error (constraint violation, deadlock, etc.) kills the entire transaction - there's no way to "recover" and continue processing other documents on that transaction. By adopting all-or-nothing semantics for all error types, the behavior is now consistent and predictable regardless of whether the error originated in a hook or in the database.

Exception: Locked document errors

Locked errors are an exception to the all-or-nothing rule. These errors occur before any database changes happen (they're a pre-flight check when overrideLock: false), so they don't affect the transaction. Bulk operations on a mix of locked and unlocked documents will return partial success: unlocked documents are processed successfully, and only the locked ones appear in errors.

When transactions are disabled (e.g., MongoDB without replica set, or disableTransaction: true): partial success is still possible since each document operates independently and there is no way to roll back operations that already happened.

When transactions are disabled (e.g., MongoDB without replica set): partial success is still possible since each document operates independently, and there is no way to roll back operations that already happened.

Behavior change for bulkOperationsSingleTransaction

This option was broken since introduction. Despite the name suggesting "single transaction", its intended purpose was to run each document in a separate transaction for DocumentDB/Cosmos DB compatibility. In practice, it only made operations run sequentially instead of in parallel while still using a shared transaction.

This fix makes the option actually work as intended: each document now gets its own isolated transaction, and operations run in parallel. All-or-nothing semantics are preserved to minimize behavioral changes if this is set (if any document fails, all transactions are rolled back). The option will be renamed to bulkOperationsSeparateTransactions in 4.0 to reflect what it actually does.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 4, 2026

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 937.61 KB 🆕 Added
packages/payload/meta_index.json esbuild/index.js 1.32 MB 🆕 Added
packages/payload/meta_shared.json esbuild/exports/shared.js 168.53 KB 🆕 Added
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 281.40 KB 🆕 Added
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.17 MB 🆕 Added
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 16.32 KB 🆕 Added
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████▌ }}}$ 82.2%, 766.88 KB
dist/views/Version ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 51.26 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 16.69 KB
dist/views/Document ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 16.18 KB
dist/views/List ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 11.28 KB
dist/views/Root ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 9.03 KB
dist/views/Versions ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 6.16 KB
dist/views/API ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 6.08 KB
dist/elements/Nav ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 5.96 KB
dist/views/Account ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 5.46 KB
dist/elements/DocumentHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 4.81 KB
dist/views/Login ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 4.40 KB
dist/views/ForgotPassword ${{\color{Goldenrod}{ }}}$ 0.3%, 3.09 KB
dist/layouts/Root ${{\color{Goldenrod}{ }}}$ 0.3%, 2.91 KB
dist/views/CreateFirstUser ${{\color{Goldenrod}{ }}}$ 0.3%, 2.81 KB
dist/templates/Default ${{\color{Goldenrod}{ }}}$ 0.3%, 2.63 KB
dist/views/BrowseByFolder ${{\color{Goldenrod}{ }}}$ 0.3%, 2.61 KB
dist/views/CollectionFolders ${{\color{Goldenrod}{ }}}$ 0.3%, 2.44 KB
dist/views/ResetPassword ${{\color{Goldenrod}{ }}}$ 0.3%, 2.40 KB
dist/views/Logout ${{\color{Goldenrod}{ }}}$ 0.2%, 1.94 KB
(other) ${{\color{Goldenrod}{ ████▍ }}}$ 17.8%, 166.05 KB

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████▉ }}}$ 67.7%, 887.18 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.3%, 43.58 KB
dist/collections/operations ${{\color{Goldenrod}{ ▊ }}}$ 3.1%, 40.43 KB
dist/versions/migrations ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 18.50 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 15.74 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.17 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 12.84 KB
dist/queues/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 12.80 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 12.07 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.54 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.77 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.35 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.74 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.69 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.54 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 6.83 KB
dist/collections/endpoints ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 6.22 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ }}}$ 0.4%, 5.58 KB
dist/auth/strategies ${{\color{Goldenrod}{ }}}$ 0.4%, 5.50 KB
dist/auth/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 5.42 KB
(other) ${{\color{Goldenrod}{ ████████ }}}$ 32.3%, 422.84 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ███████████████████▎ }}}$ 77.1%, 127.13 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▌ }}}$ 6.4%, 10.54 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 2.54 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 1.42 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.28 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 1.04 KB
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 943 B
dist/folders/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 916 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.4%, 713 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.4%, 651 B
dist/collections/config ${{\color{Goldenrod}{ }}}$ 0.3%, 570 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
dist/auth/sessions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 525 B
dist/fields/getFieldPaths.js ${{\color{Goldenrod}{ }}}$ 0.3%, 485 B
dist/utilities/getSafeRedirect.js ${{\color{Goldenrod}{ }}}$ 0.3%, 423 B
dist/utilities/deepMerge.js ${{\color{Goldenrod}{ }}}$ 0.3%, 413 B
(other) ${{\color{Goldenrod}{ █████▋ }}}$ 22.9%, 37.77 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███▏ }}}$ 12.6%, 35.14 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▉ }}}$ 11.5%, 32.00 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▏ }}}$ 8.8%, 24.36 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 23.70 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▋ }}}$ 6.8%, 18.99 KB
dist/features/link ${{\color{Goldenrod}{ █▋ }}}$ 6.5%, 18.11 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▌ }}}$ 6.4%, 17.75 KB
dist/features/upload ${{\color{Goldenrod}{ █▎ }}}$ 5.0%, 13.77 KB
dist/features/textState ${{\color{Goldenrod}{ █ }}}$ 4.0%, 11.08 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 9.03 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▊ }}}$ 3.0%, 8.22 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 7.39 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 7.15 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 7.05 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.08 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.00 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.46 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.17 KB
dist/lexical/theme ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.62 KB
dist/features/indent ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.50 KB
(other) ${{\color{Goldenrod}{ █████████████████████▊ }}}$ 87.4%, 243.00 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████▍ }}}$ 49.7%, 578.70 KB
dist/elements/FolderView ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 29.29 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 27.61 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 17.09 KB
dist/views/Edit ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 16.74 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 15.79 KB
dist/elements/Table ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 15.77 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.26 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 14.20 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 13.70 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 10.32 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 9.03 KB
dist/providers/Folders ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.45 KB
dist/elements/LivePreview ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.38 KB
dist/elements/ListHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.90 KB
dist/elements/HTMLDiff ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.81 KB
dist/fields/Array ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.73 KB
dist/views/CollectionFolder ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.38 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.34 KB
dist/views/List ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.02 KB
(other) ${{\color{Goldenrod}{ ████████████▌ }}}$ 50.3%, 586.45 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ █████ }}}$ 20.0%, 3.12 KB
../../node_modules ${{\color{Goldenrod}{ ████▎ }}}$ 17.0%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▍ }}}$ 9.8%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 862 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █▎ }}}$ 5.2%, 814 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █▏ }}}$ 4.9%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 756 B
dist/elements/Translation ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▋ }}}$ 2.6%, 399 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 339 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 329 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 301 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 159 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 152 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 147 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 146 B
(other) ${{\color{Goldenrod}{ ████████████████████ }}}$ 80.0%, 12.51 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

let docReq = req

if (useSeparateTransactions) {
docReq = isolateObjectProperty(req, 'transactionID')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Previously this

  • mutated the original req => the new transactionID leaked into consecutive doc update calls
  • did not delete the initial req.transactionID, meaning essentially this did not do anything unless the operation was called with transactions disabled
  • did not respect args.disableTransaction

// 8. Return results
// /////////////////////////////////////
if (docShouldCommit) {
await commitTransaction(req)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(This code only runs when bulkOperationsSingleTransaction is true)

We need to commit outside the loop.

Why?

If any other doc has an error and is rolled back, we do not want to commit the successful docs - instead, we want to roll them back too. This is the way this function works without bulkOperationsSingleTransaction set.

That way, we can minimize the behaviors changed by bulkOperationsSingleTransaction.

awaitedDocs = []
for (const promise of promises) {
awaitedDocs.push(await promise)
const awaitedDocs = await Promise.all(promises)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We were always running this in parallel by default. If we need a mechanism to make this sequential, we can always add another configuration option instead of stuffing side-effects like that into bulkOperationsSingleTransaction

if (existingError) {
return existingError
}
return {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Return early if there is any error. We do not want deleteUserPreferences to run, because

  • no document was actually deleted
  • the transaction was killed => deleteUserPreferences would fail if it attempts to use the existing req.transactionID


if (anyTransactionRolledBack) {
// All-or-nothing: everything was rolled back, report all as failed
const allErrors: BulkOperationResult<TSlug, TSelect>['errors'] = docs.map((doc) => {
Copy link
Copy Markdown
Member Author

@AlessioGr AlessioGr Feb 5, 2026

Choose a reason for hiding this comment

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

This is needed for correct error reporting (was not correct previously).

If any document was rolled back due to an error, all documents will be rolled back. Thus, successful document deletions were not actually successful since they will be rolled back despite no error. => Add explicit 'Transaction rolled back due to error in another document' errors here.

Previously, those documents were incorrectly returned as successful deletions/updates even though everything was rolled back!

payload,
req,
})
if (docs.length > 0) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Defensive check

throw new APIError(`Collection ${args.collection.config.slug} has disabled bulk edit`, 403)
}

const useSeparateTransactions =
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

bulkOperationsSingleTransaction is a misleading property name. We cannot rename it without a breaking change, thus this local variable with a different name, to not confuse people reading this function

const errors: BulkOperationResult<TSlug, TSelect>['errors'] = []

// Track all doc requests that have open transactions (for separate transaction mode)
const docReqsWithTransactions: PayloadRequest[] = []
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For the remainder of this file, see my comments in the delete.ts. Changes are pretty much identical

// Don't commit here - wait until all docs complete successfully
return result
} catch (error) {
docReq.payload.logger.error({ err: error, msg: `Error deleting document ${id}` })
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Without this, no error will be logged at all, which makes debugging difficult

* @returns true if a transaction was rolled back, false if no transaction existed
*/
export async function killTransaction(
req: MarkRequired<Partial<PayloadRequest>, 'payload'>,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This API mimics what we have for initTransaction - we can check the return type to determine if anything was rolled back or not.

This is necessary for accurate error reporting in delete/update.ts. If one document failed, but transactions are disabled, we do want remaining successful document updates/deletions to show as success rather than errors, as they cannot be rolled back!

Comment thread test/database/int.spec.ts
})

describe('transactions', () => {
describe('transactions', { db: 'transactionsEnabled' }, () => {
Copy link
Copy Markdown
Member Author

@AlessioGr AlessioGr Feb 5, 2026

Choose a reason for hiding this comment

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

So much simpler and easier to read.

No tests were edited here, just unwrapped everything out of .includes(process.env.PAYLOAD_DATABASE) - github doesn't diff it nicely

Comment thread test/database/int.spec.ts
where: { id: { in: docs.map((d) => d.id) } },
})

if (hasTransactions) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is so that we can correctly test it on sqlite (transactions disable) and postgres/mongo (transactions enabled)

@AlessioGr AlessioGr enabled auto-merge (squash) February 5, 2026 01:08
Comment thread test/collections-rest/int.spec.ts Outdated
})
const result = await response.json()

// With transactions enabled, when one doc fails the entire transaction is rolled back
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These tests were failing, because they were asserting for previous behavior: partial updates in bulk update/delete were possible.

After this PR, with transactions enabled, partial updates are no longer possible, thus we need to update these tests. Reasoning for changing this behavior is in PR description

@AlessioGr AlessioGr changed the title fix: log errors and fix transaction handling in bulk delete/update fix: roll back bulk delete/update transactions on partial failure instead of committing, fix bulk operation errors not being logged Feb 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes critical issues in bulk delete and update operations by implementing consistent error handling and transaction rollback behavior, and adds proper error logging that was previously missing.

Changes:

  • Bulk operations now roll back transactions and report all documents as failed when any error occurs (all-or-nothing semantics)
  • Errors during bulk operations are now properly logged instead of silently swallowed
  • Fixed bulkOperationsSingleTransaction to actually use separate transactions as intended

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/database/payload-types.ts Added new bulk-error-test collection type and changed ID types from string to number
test/database/int.spec.ts Added comprehensive tests for bulk operation error handling and transaction behavior
test/database/getConfig.ts Added test collection with hooks that throw errors for testing bulk operation failures
test/collections-rest/int.spec.ts Updated assertions to reflect new all-or-nothing transaction behavior
test/_community/payload-types.ts Changed ID types from string to number
test/__helpers/int/vitest.ts Added support for transaction-specific test filtering
packages/payload/src/utilities/killTransaction.ts Modified to return boolean indicating if transaction was rolled back
packages/payload/src/index.ts Added JSDoc documentation for bulk operations explaining all-or-nothing semantics
packages/payload/src/database/types.ts Enhanced documentation for bulkOperationsSingleTransaction option
packages/payload/src/collections/operations/update.ts Implemented error logging and all-or-nothing transaction rollback behavior
packages/payload/src/collections/operations/delete.ts Implemented error logging and all-or-nothing transaction rollback behavior
packages/db-mongodb/src/index.ts Enhanced documentation for bulkOperationsSingleTransaction option
docs/database/transactions.mdx Added documentation explaining bulk operation transaction behavior
docs/database/mongodb.mdx Updated description of bulkOperationsSingleTransaction option

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/database/int.spec.ts Outdated
Comment thread packages/payload/src/collections/operations/update.ts
Comment thread packages/payload/src/collections/operations/delete.ts
Comment thread docs/database/mongodb.mdx Outdated
AlessioGr and others added 2 commits February 4, 2026 18:38
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@AlessioGr AlessioGr requested a review from Copilot February 5, 2026 02:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/payload/src/collections/operations/update.ts
Comment thread test/database/int.spec.ts
deepshekhardas pushed a commit to deepshekhardas/payload that referenced this pull request Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants