Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9c047aa
feat(db): make indexing optional with separate entry point
kevin-dp Mar 11, 2026
1b68692
ci: apply automated fixes
autofix-ci[bot] Mar 11, 2026
e3609e7
Fix tests that need defaultIndexType after making indexing explicit
kevin-dp Mar 11, 2026
d9d6e7e
Address PR review feedback
kevin-dp Mar 12, 2026
6e8b724
Add tests for missing defaultIndexType error cases
kevin-dp Mar 12, 2026
83c65f1
Resolve merge conflicts by removing autogenerated docs
kevin-dp Mar 12, 2026
914c31a
Fix electric-db-collection tests: add defaultIndexType for eager auto…
kevin-dp Mar 12, 2026
276263d
Merge main into indexing-renaming branch
kevin-dp Mar 24, 2026
aed2b18
Merge latest main into indexing-renaming branch
kevin-dp Mar 24, 2026
46c75f5
ci: apply automated fixes
autofix-ci[bot] Mar 24, 2026
9755ffb
Fix electric.ts: use Subscription.unsubscribe() for @tanstack/store 0…
kevin-dp Mar 24, 2026
21ae848
Fix electric e2e tests: opt into indexing for pagination tests
kevin-dp Mar 24, 2026
61a7036
Fix persisted collection test: opt into indexing for index lifecycle …
kevin-dp Mar 24, 2026
4d3e95d
Fix react-db tests: opt into indexing for useLiveInfiniteQuery tests
kevin-dp Mar 24, 2026
3e1cfd8
ci: apply automated fixes
autofix-ci[bot] Mar 24, 2026
89c2bdf
Fix react-db type check: cast BTreeIndex for cross-package compatibility
kevin-dp Mar 24, 2026
db34f34
Fix index type mismatch: remove unnecessary undefined from TKey const…
kevin-dp Mar 24, 2026
69ce7d0
Fix cross-package type compatibility: remove protected from BaseIndex…
kevin-dp Mar 24, 2026
c53c40b
Remove @tanstack/db/indexing sub-path, export everything from @tansta…
kevin-dp Mar 24, 2026
48bf8b4
ci: apply automated fixes
autofix-ci[bot] Mar 24, 2026
0632b9c
Fix query-db-collection test: opt into indexing for ordered GC test
kevin-dp Mar 24, 2026
334c28a
Fix query-db-collection e2e tests: opt into indexing for all collections
kevin-dp Mar 24, 2026
a4752d5
Fix node persisted collection e2e tests: opt into indexing
kevin-dp Mar 24, 2026
cc79a42
Fix all remaining e2e tests: opt into indexing for all collections
kevin-dp Mar 24, 2026
35b9f1b
Fix Electron e2e tests: opt into indexing for persisted collections
kevin-dp Mar 24, 2026
9681751
Restore docs directory from main
kevin-dp Mar 24, 2026
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
66 changes: 66 additions & 0 deletions .changeset/optional-indexing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
'@tanstack/db': minor
---

Make indexing explicit with two index types for different use cases

**Breaking Changes:**

- `autoIndex` now defaults to `off` instead of `eager`
- `BTreeIndex` is no longer exported from `@tanstack/db` main entry point
- To use `createIndex()` or `autoIndex: 'eager'`, you must set `defaultIndexType` on the collection

**Changes:**

- New `@tanstack/db/indexing` entry point for tree-shakeable indexing
- **BasicIndex** - Lightweight index using Map + sorted Array for both equality and range queries (`eq`, `in`, `gt`, `gte`, `lt`, `lte`). O(n) updates but fast reads.
- **BTreeIndex** - Full-featured index with O(log n) updates and sorted iteration for ORDER BY optimization on large collections (10k+ items)
- Dev mode suggestions (ON by default) warn when indexes would help

**Migration:**

If you were relying on auto-indexing, set `defaultIndexType` on your collections:

1. **Lightweight indexing** (good for most use cases):

```ts
import { BasicIndex } from '@tanstack/db/indexing'

const collection = createCollection({
defaultIndexType: BasicIndex,
autoIndex: 'eager',
// ...
})
```

2. **Full BTree indexing** (for ORDER BY optimization on large collections):

```ts
import { BTreeIndex } from '@tanstack/db/indexing'

const collection = createCollection({
defaultIndexType: BTreeIndex,
autoIndex: 'eager',
// ...
})
```

3. **Per-index explicit type** (mix index types):

```ts
import { BasicIndex, BTreeIndex } from '@tanstack/db/indexing'

const collection = createCollection({
defaultIndexType: BasicIndex, // Default for createIndex()
// ...
})

// Override for specific indexes
collection.createIndex((row) => row.date, { indexType: BTreeIndex })
```

**Bundle Size Impact:**

- No indexing: ~30% smaller bundle
- BasicIndex: ~5 KB (~1.3 KB gzipped)
- BTreeIndex: ~33 KB (~7.8 KB gzipped)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, afterEach, beforeAll } from 'vitest'
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import {
createBrowserWASQLitePersistence,
persistedCollectionOptions,
Expand Down Expand Up @@ -64,6 +64,8 @@ function createPersistedCollection<T extends PersistableRow>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import { persistedCollectionOptions } from '../../src'
import { generateSeedData } from '../../../db-collection-e2e/src/fixtures/seed-data'
import type { Collection } from '@tanstack/db'
Expand Down Expand Up @@ -69,6 +69,8 @@ function createPersistedCollection<T extends PersistableRow, TDatabase>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, afterEach, beforeAll } from 'vitest'
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import { persistedCollectionOptions } from '@tanstack/db-sqlite-persisted-collection-core'
import { createNodeSQLitePersistence } from '@tanstack/db-node-sqlite-persisted-collection'
import { BetterSqlite3SQLiteDriver } from '../../db-node-sqlite-persisted-collection/src/node-driver'
Expand Down Expand Up @@ -142,6 +142,8 @@ function createPersistedCollection<T extends PersistableRow>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, afterEach, beforeAll } from 'vitest'
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import { persistedCollectionOptions } from '../src'
import { generateSeedData } from '../../db-collection-e2e/src/fixtures/seed-data'
import { runPersistedCollectionConformanceSuite } from '../../db-sqlite-persisted-collection-core/tests/contracts/persisted-collection-conformance-contract'
Expand Down Expand Up @@ -62,6 +62,8 @@ function createPersistedCollection<T extends PersistableRow>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, afterEach, beforeAll } from 'vitest'
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import BetterSqlite3 from 'better-sqlite3'
import { createNodeSQLitePersistence, persistedCollectionOptions } from '../src'
import { generateSeedData } from '../../db-collection-e2e/src/fixtures/seed-data'
Expand Down Expand Up @@ -60,6 +60,8 @@ function createPersistedCollection<T extends PersistableRow>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, afterEach, beforeAll } from 'vitest'
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import { persistedCollectionOptions } from '../src'
import { generateSeedData } from '../../db-collection-e2e/src/fixtures/seed-data'
import { runPersistedCollectionConformanceSuite } from '../../db-sqlite-persisted-collection-core/tests/contracts/persisted-collection-conformance-contract'
Expand Down Expand Up @@ -62,6 +62,8 @@ function createPersistedCollection<T extends PersistableRow>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest'
import { IR, createCollection, createTransaction } from '@tanstack/db'
import {
IR,
createCollection,
createTransaction,
BasicIndex,
} from '@tanstack/db'
import {
InvalidPersistedCollectionCoordinatorError,
InvalidPersistedStorageKeyEncodingError,
Expand Down Expand Up @@ -835,6 +840,7 @@ describe(`persistedCollectionOptions`, () => {
persistedCollectionOptions<Todo, string>({
id: `sync-present-indexes`,
getKey: (item) => item.id,
defaultIndexType: BasicIndex,
sync: {
sync: ({ markReady }) => {
markReady()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createCollection } from '@tanstack/db'
import { createCollection, BTreeIndex } from '@tanstack/db'
import { persistedCollectionOptions } from '../../src'
import { generateSeedData } from '../../../db-collection-e2e/src/fixtures/seed-data'
import type { Collection } from '@tanstack/db'
Expand Down Expand Up @@ -69,6 +69,8 @@ function createPersistedCollection<T extends PersistableRow, TDatabase>(
syncMode,
getKey: (item) => item.id,
persistence,
autoIndex: `eager`,
defaultIndexType: BTreeIndex,
}),
)

Expand Down
52 changes: 25 additions & 27 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CollectionConfigurationError,
CollectionRequiresConfigError,
CollectionRequiresSyncConfigError,
} from '../errors'
Expand All @@ -17,7 +18,7 @@ import type {
CollectionEventHandler,
CollectionIndexMetadata,
} from './events.js'
import type { BaseIndex, IndexResolver } from '../indexes/base-index.js'
import type { BaseIndex, IndexConstructor } from '../indexes/base-index.js'
import type { IndexOptions } from '../indexes/index-options.js'
import type {
ChangeMessage,
Expand All @@ -39,8 +40,6 @@ import type {
} from '../types'
import type { SingleRowRefProxy } from '../query/builder/ref-proxy'
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { BTreeIndex } from '../indexes/btree-index.js'
import type { IndexProxy } from '../indexes/lazy-index.js'
import type { WithVirtualProps } from '../virtual-props.js'

export type { CollectionIndexMetadata } from './events.js'
Expand Down Expand Up @@ -336,7 +335,16 @@ export class CollectionImpl<
// Set default values for optional config properties
this.config = {
...config,
autoIndex: config.autoIndex ?? `eager`,
autoIndex: config.autoIndex ?? `off`,
}

if (this.config.autoIndex === `eager` && !config.defaultIndexType) {
throw new CollectionConfigurationError(
`autoIndex: 'eager' requires defaultIndexType to be set. ` +
`Import an index type and set it:\n` +
` import { BasicIndex } from '@tanstack/db'\n` +
` createCollection({ defaultIndexType: BasicIndex, autoIndex: 'eager', ... })`,
)
}

this._changes = new CollectionChangesManager()
Expand All @@ -362,6 +370,7 @@ export class CollectionImpl<
this._indexes.setDeps({
state: this._state,
lifecycle: this._lifecycle,
defaultIndexType: config.defaultIndexType,
events: this._events,
})
this._lifecycle.setDeps({
Expand Down Expand Up @@ -568,38 +577,27 @@ export class CollectionImpl<
* Indexes significantly improve query performance by allowing constant time lookups
* and logarithmic time range queries instead of full scans.
*
* @template TResolver - The type of the index resolver (constructor or async loader)
* @param indexCallback - Function that extracts the indexed value from each item
* @param config - Configuration including index type and type-specific options
* @returns An index proxy that provides access to the index when ready
* @returns The created index
*
* @example
* // Create a default B+ tree index
* const ageIndex = collection.createIndex((row) => row.age)
* ```ts
* import { BasicIndex } from '@tanstack/db'
*
* // Create a ordered index with custom options
* // Create an index with explicit type
* const ageIndex = collection.createIndex((row) => row.age, {
* indexType: BTreeIndex,
* options: {
* compareFn: customComparator,
* compareOptions: { direction: 'asc', nulls: 'first', stringSort: 'lexical' }
* },
* name: 'age_btree'
* indexType: BasicIndex
* })
*
* // Create an async-loaded index
* const textIndex = collection.createIndex((row) => row.content, {
* indexType: async () => {
* const { FullTextIndex } = await import('./indexes/fulltext.js')
* return FullTextIndex
* },
* options: { language: 'en' }
* })
* // Create an index with collection's default type
* const nameIndex = collection.createIndex((row) => row.name)
* ```
*/
public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(
public createIndex<TIndexType extends IndexConstructor<TKey>>(
indexCallback: (row: SingleRowRefProxy<TOutput>) => any,
config: IndexOptions<TResolver> = {},
): IndexProxy<TKey> {
config: IndexOptions<TIndexType> = {},
): BaseIndex<TKey> {
return this._indexes.createIndex(indexCallback, config)
}

Expand All @@ -611,7 +609,7 @@ export class CollectionImpl<
* collection query planning. Existing index proxy references should be treated
* as invalid after removal.
*/
public removeIndex(indexOrId: IndexProxy<TKey> | number): boolean {
public removeIndex(indexOrId: BaseIndex<TKey> | number): boolean {
return this._indexes.removeIndex(indexOrId)
}

Expand Down
Loading
Loading