From c94e78062e1c07b6e72cebb554de27a1e8c2580d Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 10:09:23 +0300 Subject: [PATCH 1/2] fix(db): avoid undefined enumerable keys in compare options --- .../fix-compare-options-undefined-keys.md | 5 + packages/db/src/collection/index.ts | 6 +- .../db/tests/collection-auto-index.test.ts | 115 ++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-compare-options-undefined-keys.md diff --git a/.changeset/fix-compare-options-undefined-keys.md b/.changeset/fix-compare-options-undefined-keys.md new file mode 100644 index 0000000000..bb3aa33005 --- /dev/null +++ b/.changeset/fix-compare-options-undefined-keys.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +fix(db): avoid assigning undefined locale/localeOptions in buildCompareOptionsFromConfig diff --git a/packages/db/src/collection/index.ts b/packages/db/src/collection/index.ts index e51eb998d6..5c559dc8f9 100644 --- a/packages/db/src/collection/index.ts +++ b/packages/db/src/collection/index.ts @@ -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 }), } } else { return { diff --git a/packages/db/tests/collection-auto-index.test.ts b/packages/db/tests/collection-auto-index.test.ts index 4fdaac0127..12240abf48 100644 --- a/packages/db/tests/collection-auto-index.test.ts +++ b/packages/db/tests/collection-auto-index.test.ts @@ -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({ + 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({ + 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({ + 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({ + 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() + }) }) From ef25208a089a011b728b5ea520ac56b64964c6e4 Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 10:39:18 +0300 Subject: [PATCH 2/2] fix: derive localeMode from effective stringSort value --- packages/db/src/collection/index.ts | 5 ++-- .../db/tests/collection-auto-index.test.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/db/src/collection/index.ts b/packages/db/src/collection/index.ts index 5c559dc8f9..fd73b0f479 100644 --- a/packages/db/src/collection/index.ts +++ b/packages/db/src/collection/index.ts @@ -996,9 +996,10 @@ function buildCompareOptionsFromConfig( ): StringCollationConfig { if (config.defaultStringCollation) { const options = config.defaultStringCollation - const localeMode = options.stringSort === `locale` + const stringSortEffective = options.stringSort ?? `locale` + const localeMode = stringSortEffective === `locale` return { - stringSort: options.stringSort ?? `locale`, + stringSort: stringSortEffective, ...(localeMode && options.locale !== undefined && { locale: options.locale }), ...(localeMode && options.localeOptions !== undefined && { localeOptions: options.localeOptions }), } diff --git a/packages/db/tests/collection-auto-index.test.ts b/packages/db/tests/collection-auto-index.test.ts index 12240abf48..4d5b55c88e 100644 --- a/packages/db/tests/collection-auto-index.test.ts +++ b/packages/db/tests/collection-auto-index.test.ts @@ -1010,6 +1010,29 @@ describe(`Collection Auto-Indexing`, () => { 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({ + 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({ getKey: (item) => item.id,