Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-compare-options-undefined-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/db": patch
---

fix(db): avoid assigning undefined locale/localeOptions in buildCompareOptionsFromConfig
9 changes: 5 additions & 4 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,11 +996,12 @@ function buildCompareOptionsFromConfig(
): StringCollationConfig {
if (config.defaultStringCollation) {
const options = config.defaultStringCollation
const stringSortEffective = options.stringSort ?? `locale`
const localeMode = stringSortEffective === `locale`
return {
stringSort: options.stringSort ?? `locale`,
locale: options.stringSort === `locale` ? options.locale : undefined,
localeOptions:
options.stringSort === `locale` ? options.localeOptions : undefined,
stringSort: stringSortEffective,
...(localeMode && options.locale !== undefined && { locale: options.locale }),
...(localeMode && options.localeOptions !== undefined && { localeOptions: options.localeOptions }),
}
} else {
return {
Expand Down
138 changes: 138 additions & 0 deletions packages/db/tests/collection-auto-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,4 +942,142 @@ describe(`Collection Auto-Indexing`, () => {
})
})
})

it(`should not include undefined locale/localeOptions keys in compareOptions when stringSort is not locale`, () => {
const collection = createCollection<TestItem, string>({
getKey: (item) => item.id,
defaultStringCollation: { stringSort: `codepoint` },
startSync: true,
sync: {
sync: ({ begin, commit, markReady }) => {
begin()
commit()
markReady()
},
},
})

const opts = collection.compareOptions
expect(opts.stringSort).toBe(`codepoint`)
expect(Object.keys(opts)).not.toContain(`locale`)
expect(Object.keys(opts)).not.toContain(`localeOptions`)
expect(Object.keys(opts)).toEqual([`stringSort`])
})

it(`should not include undefined locale/localeOptions keys when stringSort is locale without explicit locale`, () => {
const collection = createCollection<TestItem, string>({
getKey: (item) => item.id,
defaultStringCollation: { stringSort: `locale` },
startSync: true,
sync: {
sync: ({ begin, commit, markReady }) => {
begin()
commit()
markReady()
},
},
})

const opts = collection.compareOptions
expect(opts.stringSort).toBe(`locale`)
expect(Object.keys(opts)).not.toContain(`locale`)
expect(Object.keys(opts)).not.toContain(`localeOptions`)
expect(Object.keys(opts)).toEqual([`stringSort`])
})

it(`should include locale and localeOptions keys when explicitly provided`, () => {
const collection = createCollection<TestItem, string>({
getKey: (item) => item.id,
defaultStringCollation: {
stringSort: `locale`,
locale: `en-US`,
localeOptions: { sensitivity: `base` },
},
startSync: true,
sync: {
sync: ({ begin, commit, markReady }) => {
begin()
commit()
markReady()
},
},
})

const opts = collection.compareOptions
expect(opts.stringSort).toBe(`locale`)
expect(opts.locale).toBe(`en-US`)
expect(opts.localeOptions).toEqual({ sensitivity: `base` })
expect(Object.keys(opts).sort()).toEqual([`locale`, `localeOptions`, `stringSort`])
})

it(`should include locale fields when stringSort is omitted but locale values are provided`, () => {
const collection = createCollection<TestItem, string>({
getKey: (item) => item.id,
defaultStringCollation: {
locale: `en-US`,
localeOptions: { sensitivity: `base` },
},
startSync: true,
sync: {
sync: ({ begin, commit, markReady }) => {
begin()
commit()
markReady()
},
},
})

const opts = collection.compareOptions
expect(opts.stringSort).toBe(`locale`)
expect(opts.locale).toBe(`en-US`)
expect(opts.localeOptions).toEqual({ sensitivity: `base` })
})

it(`should not create duplicate auto-indexes when defaultStringCollation matches defaults`, async () => {
const collection = createCollection<TestItem, string>({
getKey: (item) => item.id,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
defaultStringCollation: { stringSort: `locale` },
startSync: true,
sync: {
sync: ({ begin, write, commit, markReady }) => {
begin()
for (const item of testData) {
write({ type: `insert`, value: item })
}
commit()
markReady()
},
},
})

await collection.stateWhenReady()

const sub1 = collection.subscribeChanges(() => {}, {
whereExpression: eq(row.status, `active`),
})
expect(collection.indexes.size).toBe(1)

// A second subscription on the same field should reuse the index
const sub2 = collection.subscribeChanges(() => {}, {
whereExpression: eq(row.status, `inactive`),
})
expect(collection.indexes.size).toBe(1)

// Verify the index is used for queries
withIndexTracking(collection, (tracker) => {
collection.currentStateAsChanges({
where: eq(row.status, `active`),
})

expectIndexUsage(tracker.stats, {
shouldUseIndex: true,
shouldUseFullScan: false,
})
})

sub1.unsubscribe()
sub2.unsubscribe()
})
})