Skip to content

Commit 5c935aa

Browse files
fix(db-mongodb): fix pagination with collation in transactions (#15990)
## Summary Fixes incorrect `totalDocs` count when using MongoDB collation with sessions in transactions. ## Problem When `collation` is enabled on the MongoDB adapter, `mongoose-paginate-v2` chains `.collation()` on queries which breaks session context in mongoose 8.x. This causes: - Incorrect `totalDocs` (returns limit value instead of actual count) - Transaction mismatch errors in some cases ## Solution - Add custom count function (`useCustomCountFn`) when collation is enabled to pass collation as an option instead of chaining - Use `config.localization.defaultLocale` for collation locale fallback instead of hardcoded `'en'` - Add collation support to count functions ## Upstream Fix Submitted a fix to mongoose-paginate-v2: aravindnc/mongoose-paginate-v2#240 The workaround in this PR includes TODO comments indicating it can be removed once the upstream fix is merged and released. ## Test plan - [x] Added integration test that verifies collation works with draft pagination - [x] Existing database tests pass Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 67063ab commit 5c935aa

File tree

8 files changed

+158
-4
lines changed

8 files changed

+158
-4
lines changed

packages/db-mongodb/src/count.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ export const count: Count = async function count(
3737
session: await getSession(this, req),
3838
}
3939

40+
if (this.collation) {
41+
const localizationConfig = this.payload.config.localization
42+
const defaultLocale =
43+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
44+
45+
options.collation = {
46+
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
47+
...this.collation,
48+
}
49+
}
50+
4051
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
4152
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
4253
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,

packages/db-mongodb/src/countGlobalVersions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
3636
session: await getSession(this, req),
3737
}
3838

39+
if (this.collation) {
40+
const localizationConfig = this.payload.config.localization
41+
const defaultLocale =
42+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
43+
44+
options.collation = {
45+
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
46+
...this.collation,
47+
}
48+
}
49+
3950
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
4051
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
4152
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,

packages/db-mongodb/src/countVersions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ export const countVersions: CountVersions = async function countVersions(
4040
session: await getSession(this, req),
4141
}
4242

43+
if (this.collation) {
44+
const localizationConfig = this.payload.config.localization
45+
const defaultLocale =
46+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
47+
48+
options.collation = {
49+
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
50+
...this.collation,
51+
}
52+
}
53+
4354
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
4455
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
4556
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,

packages/db-mongodb/src/find.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ export const find: Find = async function find(
9090
}
9191

9292
if (this.collation) {
93-
const defaultLocale = 'en'
93+
const localizationConfig = this.payload.config.localization
94+
const defaultLocale =
95+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
96+
9497
paginationOptions.collation = {
9598
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
9699
...this.collation,
@@ -105,11 +108,25 @@ export const find: Find = async function find(
105108
paginationOptions.useCustomCountFn = () => {
106109
return Promise.resolve(
107110
Model.countDocuments(query, {
111+
collation: paginationOptions.collation,
108112
hint: { _id: 1 },
109113
session,
110114
}),
111115
)
112116
}
117+
} else if (!useEstimatedCount && this.collation) {
118+
// Workaround for mongoose-paginate-v2 bug: chaining .collation() on countDocuments breaks
119+
// session context in transactions (mongoose 8.x). Provide custom count function that passes
120+
// collation as an option instead. See: https://github.com/aravindnc/mongoose-paginate-v2/pull/240
121+
// TODO: Remove this workaround once mongoose-paginate-v2 is updated with the fix.
122+
paginationOptions.useCustomCountFn = () => {
123+
return Promise.resolve(
124+
Model.countDocuments(query, {
125+
collation: paginationOptions.collation,
126+
session,
127+
}),
128+
)
129+
}
113130
}
114131

115132
if (limit >= 0) {

packages/db-mongodb/src/findGlobalVersions.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
8080
}
8181

8282
if (this.collation) {
83-
const defaultLocale = 'en'
83+
const localizationConfig = this.payload.config.localization
84+
const defaultLocale =
85+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
86+
8487
paginationOptions.collation = {
8588
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
8689
...this.collation,
@@ -95,11 +98,25 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
9598
paginationOptions.useCustomCountFn = () => {
9699
return Promise.resolve(
97100
Model.countDocuments(query, {
101+
collation: paginationOptions.collation,
98102
hint: { _id: 1 },
99103
session,
100104
}),
101105
)
102106
}
107+
} else if (!useEstimatedCount && this.collation) {
108+
// Workaround for mongoose-paginate-v2 bug: chaining .collation() on countDocuments breaks
109+
// session context in transactions (mongoose 8.x). Provide custom count function that passes
110+
// collation as an option instead. See: https://github.com/aravindnc/mongoose-paginate-v2/pull/240
111+
// TODO: Remove this workaround once mongoose-paginate-v2 is updated with the fix.
112+
paginationOptions.useCustomCountFn = () => {
113+
return Promise.resolve(
114+
Model.countDocuments(query, {
115+
collation: paginationOptions.collation,
116+
session,
117+
}),
118+
)
119+
}
103120
}
104121

105122
if (limit >= 0) {

packages/db-mongodb/src/findVersions.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ export const findVersions: FindVersions = async function findVersions(
8888
}
8989

9090
if (this.collation) {
91-
const defaultLocale = 'en'
91+
const localizationConfig = this.payload.config.localization
92+
const defaultLocale =
93+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
94+
9295
paginationOptions.collation = {
9396
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
9497
...this.collation,
@@ -103,11 +106,25 @@ export const findVersions: FindVersions = async function findVersions(
103106
paginationOptions.useCustomCountFn = () => {
104107
return Promise.resolve(
105108
Model.countDocuments(query, {
109+
collation: paginationOptions.collation,
106110
hint: { _id: 1 },
107111
session,
108112
}),
109113
)
110114
}
115+
} else if (!useEstimatedCount && this.collation) {
116+
// Workaround for mongoose-paginate-v2 bug: chaining .collation() on countDocuments breaks
117+
// session context in transactions (mongoose 8.x). Provide custom count function that passes
118+
// collation as an option instead. See: https://github.com/aravindnc/mongoose-paginate-v2/pull/240
119+
// TODO: Remove this workaround once mongoose-paginate-v2 is updated with the fix.
120+
paginationOptions.useCustomCountFn = () => {
121+
return Promise.resolve(
122+
Model.countDocuments(query, {
123+
collation: paginationOptions.collation,
124+
session,
125+
}),
126+
)
127+
}
111128
}
112129

113130
if (limit >= 0) {

packages/db-mongodb/src/queryDrafts.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
9595
}
9696

9797
if (this.collation) {
98-
const defaultLocale = 'en'
98+
const localizationConfig = this.payload.config.localization
99+
const defaultLocale =
100+
(typeof localizationConfig === 'object' && localizationConfig?.defaultLocale) || 'en'
101+
99102
paginationOptions.collation = {
100103
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
101104
...this.collation,
@@ -114,7 +117,22 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
114117
paginationOptions.useCustomCountFn = () => {
115118
return Promise.resolve(
116119
Model.countDocuments(versionQuery, {
120+
collation: paginationOptions.collation,
117121
hint: { _id: 1 },
122+
session,
123+
}),
124+
)
125+
}
126+
} else if (!useEstimatedCount && this.collation) {
127+
// Workaround for mongoose-paginate-v2 bug: chaining .collation() on countDocuments breaks
128+
// session context in transactions (mongoose 8.x). Provide custom count function that passes
129+
// collation as an option instead. See: https://github.com/aravindnc/mongoose-paginate-v2/pull/240
130+
// TODO: Remove this workaround once mongoose-paginate-v2 is updated with the fix.
131+
paginationOptions.useCustomCountFn = () => {
132+
return Promise.resolve(
133+
Model.countDocuments(versionQuery, {
134+
collation: paginationOptions.collation,
135+
session,
118136
}),
119137
)
120138
}

test/database/int.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5691,4 +5691,56 @@ describe('database', () => {
56915691
expect(collatedMappedResults).toEqual(expectedSortedItems)
56925692
},
56935693
)
5694+
5695+
it(
5696+
'ensure mongodb collation works with draft pagination without sort',
5697+
{ db: 'mongo' },
5698+
async () => {
5699+
// Clear any existing documents
5700+
await payload.delete({ collection: 'categories', where: {} })
5701+
5702+
// Create 15 draft documents
5703+
const createdIds: (number | string)[] = []
5704+
for (let i = 0; i < 15; i++) {
5705+
const doc = await payload.create({
5706+
collection: 'categories',
5707+
data: { name: `Category ${i}` },
5708+
draft: true,
5709+
})
5710+
createdIds.push(doc.id)
5711+
}
5712+
5713+
// Enable collation
5714+
payload.db.collation = { strength: 2 }
5715+
5716+
// Query drafts WITHOUT sort - this is the scenario that breaks
5717+
const resultsNoSort = await payload.find({
5718+
collection: 'categories',
5719+
limit: 10,
5720+
draft: true,
5721+
// No sort parameter
5722+
})
5723+
5724+
console.log({
5725+
totalDocs: resultsNoSort.totalDocs,
5726+
totalPages: resultsNoSort.totalPages,
5727+
docsLength: resultsNoSort.docs.length,
5728+
hasNextPage: resultsNoSort.hasNextPage,
5729+
})
5730+
5731+
// The bug: totalDocs returns 10 (same as limit) instead of 15
5732+
expect(resultsNoSort.totalDocs).toBe(15)
5733+
expect(resultsNoSort.totalPages).toBe(2)
5734+
expect(resultsNoSort.hasNextPage).toBe(true)
5735+
expect(resultsNoSort.docs.length).toBe(10)
5736+
5737+
// Clean up
5738+
for (const id of createdIds) {
5739+
await payload.delete({ collection: 'categories', id })
5740+
}
5741+
5742+
// Reset collation
5743+
payload.db.collation = undefined
5744+
},
5745+
)
56945746
})

0 commit comments

Comments
 (0)