Skip to content

test(orm): count documents scanned behind .filter() - #399

Merged
zbeyens merged 6 commits into
mainfrom
test/scan-aware-read-counting
Aug 25, 2026
Merged

zbeyens merged 6 commits into
mainfrom
test/scan-aware-read-counting

Conversation

@MikeyZhang75

@MikeyZhang75 MikeyZhang75 commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

🐛 Fixes #384

🧭 Task plan: docs/plans/2026-08-21-384-scan-aware-read-counting-for-filter-paths.md

🟢 95-100% confidence

Phase 🧪 Tests 🌐 Browser
Reproduced 🔴 Filtered scans reported only returned rows ➖ N/A
Verified 🟢 19 focused tests and full bun check ➖ N/A

✅ Outcome

  • countDocumentReads reports returned documents and source rows scanned.
  • Filtered terminals charge rejected rows through their actual stopping boundary.
  • Pagination uses opaque cursor ranges, including continuation and scan-limited pages.
  • Leading source limits remain active; unsupported operator interleavings fail instead of under-reporting.
  • Existing ORM read bounds assert the billed scanned metric.

⚠️ Caveat

  • Cross-index OR full scans are measured, not optimized.
  • Scan replay depends on the pinned Convex query-plan shape.
  • The simulator cannot model arbitrary interleaved serialized limits, so those shapes throw.

🏗️ Design

  • The counter replays the source through Convex instead of reimplementing filter evaluation.
  • Cursor-bounded pagination advances one source row per replay page for exact opaque-boundary accounting.
  • Leading limits stay active because they bound the source before filtering.

🧪 Verified

  • 19 focused read-accounting tests; no type errors.
  • Full bun check, including fixture parity and every runtime scenario.
  • Exact-head P1 autoreview clean at 0.88 confidence.

countDocumentReads counted rows returned, never rows Convex read and a
.filter() rejected, so read bounds on filter paths could not fail. Adds
scanned alongside documents: reads the serialized plan off
QueryImpl.state.query before the terminal consumes it, replays the source
with no operators, and charges the walk. Early stop modelled, unrecognized
plan shapes throw instead of under-reporting.

Moves every read bound to scanned, re-seeds M4 at N=300.

Fixes #384
@changeset-bot

changeset-bot Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: fee68a9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@vercel

vercel Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-convex Ready Ready Preview Aug 25, 2026 8:44pm

Request Review

Conflict in convex/setup.testing.ts JSDoc: kept both sides. main's
install-before-withOrm warning is retained, retargeted at reads.scanned.

main added four read bounds on reads.documents (pipeline union,
invite-member seats, relation-depth ceiling, comment tree). Moved them to
scanned so the newest bounds do not carry the blindness #384 removed.

comment-tree marginal cost is 80 scanned vs 60 returned, so its per-node
constant goes 2 -> 3. The gap is real: todoComments indexes todoId and
parentId separately, so parentId isNull runs as a Convex .filter() that
reads every reply to find the roots.
@zbeyens

zbeyens commented Aug 25, 2026

Copy link
Copy Markdown
Member

Autoclosure receipt

  • Exact head: fee68a9e62b9fd245b2bf472d5badc7640d2e1e0
  • Per-PR task plan: docs/plans/2026-08-21-384-scan-aware-read-counting-for-filter-paths.md
  • Focused proof: 19/19 read-accounting tests; no type errors
  • Local gate: bun lint:fix && bun --cwd packages/kitcn build && bun check exited 0, including fixture parity and every runtime scenario
  • Exact-head P1 autoreview: clean, patch correct at 0.88 confidence
  • Hosted CI: source, type, Bun, Vitest, CLI, and Concave lanes passed; fixture installation was externally blocked twice by unpublished Expo SDK 55 dependencies (run)
  • Vercel: passed
  • Feedback audit: helper 2 informational comments; raw inventory 3 informational comments; zero reviews, zero review threads, zero actionable P1+, and no deferred P2/P3
  • Informational bots: changeset — no package change; Codex quota — operational notice; Vercel — deployment passed

@zbeyens
zbeyens merged commit 475c579 into main Aug 25, 2026
7 of 9 checks passed
MikeyZhang75 added a commit that referenced this pull request Aug 25, 2026
Resolves the countDocumentReads collision flagged in PR #422: #399 landed a
scanned-row counter on main that reads QueryImpl.state.query and walks a
paginated range cursor by cursor. That is strictly better than this branch's
chain-replay version, so convex/setup.testing.ts now matches main exactly and
the cascade read-bound tests assert on main's counter.

Both counters independently report 1100 scanned rows for the 100-child repro
and 4214 for the byte-budget one, so the bounds are cross-validated, not tuned
to one implementation. Red calibration re-run against main's pre-fix worker:
all three cases still fail.

This branch was successfully deployed

1 active deployment
Preview — fee68a9e Deployed Aug 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Testing: countDocumentReads counts rows returned, not rows scanned behind .filter() — read-bound assertions on filter paths prove nothing

2 participants