fix: deterministic code-point string collation for column store + aggregation
Replace localeCompare (default-locale, non-deterministic across environments — unsafe for a persisted sorted index) with UTF-8 byte / code-point order via a shared compareCodePoints() helper. String ordering is now deterministic and byte-identical to the native column store / aggregation sort, so results are the same with or without the native accelerator. Covers the aggregate orderBy sort and all 5 column-store comparison sites (tail buffer, merge sort, binary search). Adds compareCodePoints unit tests.
This commit is contained in:
parent
fe4f5df8c9
commit
547721ae14
6 changed files with 92 additions and 6 deletions
|
|
@ -14,6 +14,7 @@
|
|||
import type { SegmentHeader } from './types.js'
|
||||
import { ValueType } from './types.js'
|
||||
import type { RoaringBitmap32 } from '../../utils/roaring/index.js'
|
||||
import { compareCodePoints } from '../../utils/collation.js'
|
||||
|
||||
/**
|
||||
* Binary search result.
|
||||
|
|
@ -100,7 +101,7 @@ export class ColumnSegmentCursor {
|
|||
while (lo < hi) {
|
||||
const mid = (lo + hi) >>> 1
|
||||
const cmp = isString
|
||||
? String(this.values[mid]).localeCompare(String(target))
|
||||
? compareCodePoints(String(this.values[mid]), String(target))
|
||||
: (this.values[mid] as number) - (target as number)
|
||||
|
||||
if (cmp < 0) {
|
||||
|
|
@ -136,7 +137,7 @@ export class ColumnSegmentCursor {
|
|||
while (endLo < endHi) {
|
||||
const mid = (endLo + endHi) >>> 1
|
||||
const cmp = isString
|
||||
? String(this.values[mid]).localeCompare(String(hi))
|
||||
? compareCodePoints(String(this.values[mid]), String(hi))
|
||||
: (this.values[mid] as number) - (hi as number)
|
||||
if (cmp <= 0) {
|
||||
endLo = mid + 1
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { ColumnManifest } from './ColumnManifest.js'
|
|||
import { ColumnSegmentCursor, TailBufferCursor, type CursorEntry } from './ColumnSegmentCursor.js'
|
||||
import { writeSegmentToBuffer, readSegmentFromBuffer } from './ColumnSegmentFormat.js'
|
||||
import { RoaringBitmap32 } from '../../utils/roaring/index.js'
|
||||
import { compareCodePoints } from '../../utils/collation.js'
|
||||
|
||||
/**
|
||||
* Configuration for the ColumnStore.
|
||||
|
|
@ -554,7 +555,7 @@ export class ColumnStore implements ColumnStoreProvider {
|
|||
// Sort a copy for the cursor
|
||||
const sorted = entries.slice().sort((a, b) => {
|
||||
if (valueType === ValueType.String) {
|
||||
return String(a.value).localeCompare(String(b.value))
|
||||
return compareCodePoints(String(a.value), String(b.value))
|
||||
}
|
||||
return (a.value as number) - (b.value as number)
|
||||
})
|
||||
|
|
@ -615,7 +616,7 @@ export class ColumnStore implements ColumnStoreProvider {
|
|||
const compare = (a: HeapEntry, b: HeapEntry): number => {
|
||||
let cmp: number
|
||||
if (isString) {
|
||||
cmp = String(a.value).localeCompare(String(b.value))
|
||||
cmp = compareCodePoints(String(a.value), String(b.value))
|
||||
} else {
|
||||
cmp = (a.value as number) - (b.value as number)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
*/
|
||||
|
||||
import { ValueType, DEFAULT_FLUSH_THRESHOLD } from './types.js'
|
||||
import { compareCodePoints } from '../../utils/collation.js'
|
||||
|
||||
/**
|
||||
* Entry in the tail buffer: a (value, entityIntId) pair.
|
||||
|
|
@ -131,7 +132,7 @@ export class ColumnTailBuffer {
|
|||
const sorted = this.entries.slice()
|
||||
if (this.valueType === ValueType.String) {
|
||||
sorted.sort((a, b) => {
|
||||
const cmp = String(a.value).localeCompare(String(b.value))
|
||||
const cmp = compareCodePoints(String(a.value), String(b.value))
|
||||
return cmp !== 0 ? cmp : a.entityIntId - b.entityIntId
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue