fix: orderable has incorrect sort results depending on capitalization (#12758)
### What? The results when querying orderable collections can be incorrect due to how the underlying database handles sorting when capitalized letters are introduced. ### Why? The original fractional indexing logic uses base 62 characters to maximize the amount of data per character. This optimization saves a few characters of text in the database but fails to return accurate results when mixing uppercase and lowercase characters. ### How? Instead we can use base 36 values instead (0-9,a-z) so that all databases handle the sort consistently without needing to introduce collation or other alternate solutions. Fixes #12397
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
|
||||
// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
|
||||
|
||||
export const BASE_62_DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
export const BASE_36_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
// `a` may be empty string, `b` is null or non-empty string.
|
||||
// `a < b` lexicographically if `b` is non-null.
|
||||
@@ -215,7 +215,7 @@ function decrementInteger(x, digits) {
|
||||
* @param {string=} digits
|
||||
* @return {string}
|
||||
*/
|
||||
export function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
||||
export function generateKeyBetween(a, b, digits = BASE_36_DIGITS) {
|
||||
if (a != null) {
|
||||
validateOrderKey(a, digits)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ export function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
||||
* @param {string} digits
|
||||
* @return {string[]}
|
||||
*/
|
||||
export function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
|
||||
export function generateNKeysBetween(a, b, n, digits = BASE_36_DIGITS) {
|
||||
if (n === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -628,6 +628,69 @@ describe('Sort', () => {
|
||||
parseInt(docDuplicatedAfterReorder._order!, 16),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not break with existing base 62 digits', async () => {
|
||||
const collection = orderableSlug
|
||||
// create seed docs with aa, aA, AA
|
||||
const aa = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: 'Base62 aa',
|
||||
_order: 'aa',
|
||||
},
|
||||
})
|
||||
const aA = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: 'Base62 aA',
|
||||
_order: 'aA',
|
||||
},
|
||||
})
|
||||
const AA = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: 'Base62 AA',
|
||||
_order: 'AA',
|
||||
},
|
||||
})
|
||||
|
||||
const orderableDoc = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
title: 'Base62 new',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await restClient.POST('/reorder', {
|
||||
body: JSON.stringify({
|
||||
collectionSlug: orderableSlug,
|
||||
docsToMove: [orderableDoc.id],
|
||||
newKeyWillBe: 'greater',
|
||||
orderableFieldName: '_order',
|
||||
target: {
|
||||
id: aA.id,
|
||||
key: aA._order,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(res.status).toStrictEqual(200)
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection,
|
||||
sort: '-_order',
|
||||
where: {
|
||||
title: {
|
||||
contains: 'Base62 ',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(docs[0]?.id).toStrictEqual(aa.id)
|
||||
expect(docs[1]?.id).toStrictEqual(aA.id)
|
||||
expect(docs[2]?.id).toStrictEqual(orderableDoc.id)
|
||||
expect(docs[3]?.id).toStrictEqual(AA.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Orderable join', () => {
|
||||
|
||||
Reference in New Issue
Block a user