Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .agents/skills/kitcn/references/features/orm.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,23 @@ const page2 = await ctx.orm.query.posts.findMany({

Return: `{ page, continueCursor, isDone, pageStatus?, splitCursor? }`

### Index-union filters

`in`, `notIn`, `ne`, and same-field equality `OR` compile to one index range per value when the field is one an index leads with. Cursor pages come from those ranges as a merged stream, so `orderBy` sorts across the whole result and no scan budget is needed.

```ts
const page = await ctx.orm.query.users.withIndex("by_status").findMany({
where: { status: { in: ["active", "pending"] } },
orderBy: { createdAt: "desc" },
cursor: null,
limit: 20,
});
```

Falls back to a bounded scan (needs `maxScan`) when the probed index cannot supply the requested `orderBy`, or when the union is wider than 64 ranges.

Without an `orderBy`, an index-union page is in the order of the index it walks β€” grouped by the probed value β€” not in creation order.

### Boundary pinning with `endCursor`

```ts
Expand Down Expand Up @@ -736,7 +753,7 @@ const filtered = results.filter((a) => a.publishedAt >= startDate);
### Performance

1. **Index first** β€” constrain leading index fields. Compound indexes follow prefix rules. Put the `orderBy` column right after the constrained prefix so the scan is already sorted and `limit` bounds the read.
2. **Bound scans** β€” use `maxScan` for predicate `where` (cursor mode only). A `.select()` ID-list query with `orderBy` needs a single field that an index leads with.
2. **Bound scans** β€” use `maxScan` for predicate `where` (cursor mode only). `in`/`notIn`/`ne` on an indexed leading field page from index ranges and need no budget. A `.select()` ID-list query with `orderBy` needs a single field that an index leads with.
3. **Limit results** β€” always use `limit` or cursor pagination.
4. **Cursor stability** β€” keep same `where`/`orderBy` between page requests.
5. **`allowFullScan`** β€” non-cursor only. Cursor mode uses `maxScan` instead.
Expand Down
37 changes: 37 additions & 0 deletions .changeset/orm-index-union-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"kitcn": minor
---

## Breaking changes

- Cursor pages for an index-union filter with no `orderBy` are now in the order of the index the read walks, grouped by the probed value, instead of creation order. Add `orderBy` to keep newest-first paging.

```ts
// Before
const page = await db.query.users.withIndex('by_status').findMany({
where: { status: { in: ['active', 'pending'] } },
cursor: null,
limit: 20,
maxScan: 500,
});

// After
const page = await db.query.users.withIndex('by_status').findMany({
where: { status: { in: ['active', 'pending'] } },
orderBy: { createdAt: 'desc' },
cursor: null,
limit: 20,
});
```

## Features

- Page `in`, `notIn`, `ne`, and same-field equality `OR` filters from one index range per value instead of scanning the table. Cursor pagination over these filters no longer needs `maxScan`, and `orderBy` sorts across the whole result rather than per value.

## Patches

- Fix `select()` composition and `endCursor` pagination reading a whole index instead of the compiled index ranges when the filter is an index union.
- Keep no-`orderBy` cursor direction consistent when `endCursor` routes an index union through the advanced stream path.
- Prevent `endCursor` narrowing from reopening disjoint equality ranges and duplicating merged-stream rows.
- Fix cursor pagination with a residual post-filter reading a whole index instead of the compiled index ranges when the filter is an index union.
- Fall back to a bounded scan when the probed index cannot supply the requested `orderBy` or the union is wider than 64 ranges.
Loading
Loading