Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,11 +996,11 @@ function buildCompareOptionsFromConfig(
): StringCollationConfig {
if (config.defaultStringCollation) {
const options = config.defaultStringCollation
const localeMode = options.stringSort === `locale`
return {
stringSort: options.stringSort ?? `locale`,
locale: options.stringSort === `locale` ? options.locale : undefined,
localeOptions:
options.stringSort === `locale` ? options.localeOptions : undefined,
...(localeMode && options.locale !== undefined && { locale: options.locale }),
...(localeMode && options.localeOptions !== undefined && { localeOptions: options.localeOptions }),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
} else {
return {
Expand Down
115 changes: 115 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,119 @@ 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 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()
})
})