Skip to content

Collection

A query that has not run yet — build it up, then execute it with toArray(), count() and friends.

⚠️ Two easily-confused methods

MethodReturns
sortBy(keyPath)a sorted arrayPromise<T[]>, not a Collection
keys()the index keys — not the primary keys (use primaryKeys() for those)

Both are asserted in the test suite, because getting either wrong fails silently rather than loudly.

Filtering & ordering

MethodReturnsNotes
or(index)WhereClauseUnion with another condition
filter(fn) / and(fn)CollectionJS predicate, client-side
until(fn, includeStop = false)CollectionStop at the first match
limit(n) / offset(n)Collection
reverse()CollectionFlip current direction — of the bound index, see below
desc()CollectionForce descending
orderBy(index)CollectionChainable ordering
distinct()CollectionNo-op — see below

Iteration order

A collection iterates its bound index: the orderBy index if you set one, otherwise the first index you filtered on, otherwise the primary key. reverse() flips that. This matters most for paging — offset/limit over a different order returns different rows, not merely a different arrangement.

js
// ordered by age descending, not by id
await db.friends.where('age').above(18).reverse().offset(20).limit(10).toArray();

Rows whose key is absent or null are not in the index, so orderBy('nickname') omits anyone without a nickname — matching IndexedDB, where such records are never added to that index. The same rule applies to notEqual, noneOf and startsWith('').

sortBy(keyPath) is different: it sorts client-side over whatever the collection returned, so it keeps rows with no key (they sort last) and works on any keyPath, indexed or not.

Filter on one index, sort by another

A cursor-based store can only use a single index per query. A SQL query planner can filter on one index and order by another, so this just works:

js
await db.issues
  .where('key').anyOf(['a', 'b'])   // filtered by one index
  .orderBy('updated_at')            // ordered by another
  .offset(20).limit(10)
  .toArray();

distinct() is a no-op

Dexie needs it because its multiEntry cursor yields one hit per matching array element. Our multiEntry compiles to an IN (SELECT ...) subquery which never duplicates rows. The method exists so migrated code runs unchanged.

Reading

MethodReturns
toArray()Promise<T[]>
first() / last()Promise<T | undefined>
count()Promise<number>
toMap(keyPath?)Promise<Map>
primaryKeys()Promise<Key[]>
keys()Promise<IndexKey[]>index keys
uniqueKeys()Promise<IndexKey[]>
firstKey() / lastKey()Promise<IndexKey>
sortBy(keyPath)Promise<T[]> — accepts any keyPath, not just an index
each(fn) / eachKey(fn) / eachPrimaryKey(fn) / eachUniqueKey(fn)Promise<void>

for await (const doc of collection) { ... } also works.

Writing

delete()Promise<number>

modify(changes)Promise<number>

Object form compiles to a single json_patch UPDATE:

js
await db.friends.where('age').below(18).modify({ junior: true });

Function form matches Dexie — it reads the matching docs, applies the function, and writes them back in one atomic batch:

js
await db.friends.where('age').below(18).modify((f) => { f.junior = true; });

// delete rows by clearing ctx.value, as in Dexie
await db.friends.where('age').above(99).modify(function (f, ctx) { ctx.value = undefined; });

Performance note

.filter(fn) and .until(fn) run in JS, so the worker returns every index match and limit/offset apply afterwards. Narrow with an indexed .where() first on large tables.