Skip to content

fix(drivers): limit: 0 returns no records, on every driver and every read door (#6577) - #6793

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-6577-sql-limit-presence
Aug 8, 2026
Merged

fix(drivers): limit: 0 returns no records, on every driver and every read door (#6577)#6793
os-zhuang merged 6 commits into
mainfrom
claude/issue-6577-sql-limit-presence

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #6577

limit: 0 was ruled in #6485 to mean return no records. Three of the five shipped drivers did not honour it — in three genuinely different ways — and every one of them answered with more data than was requested, which on an ADR-0021 RLS read scope is over-reach rather than a loose filter (#3948).

Reachable rather than theoretical: since #6578 the client puts top=0 on the wire, so the answer depended on which driver the deployment configured.

Scope — the maintainer's #5499 freeze exception

Dispatched as the unfrozen driver-sql half, then re-scoped to the full card by the maintainer ruling on #6577 (comment 5226535233, 2026-08-08T14:27Z), verbatim: 「A+ 批准,按这个范围执行」. The exception is the limit door only; the #5499 freeze remains in force for everything else in driver-memory / driver-mongodb — the #6682 case-folding programme is untouched, and both packages keep their FILTER_TEXT_CASES DEBT rows exactly as #6706 left them.

Premise re-verified against post-#6706 origin/main, measured not read

Hard precondition honoured: #6706 (issue #6518) touches the same file and was still open when this was dispatched. Polled origin/main until its merge commit 3172831 landed at 14:21:57Z; branched from that commit, never earlier. The two truthiness doors had drifted again (triage said :3922/:3962, #6706 moved them to :4197/:4237) — located by the if (query.limit) builder.limit(query.limit) pattern, never by line number.

Probed on 3172831 before any line moved, three rows seeded:

PROBE sql-driver     find    { limit: 0 } -> 0 rows   select * from `orders` order by `id` asc limit ?
                     window  { limit: 0 } -> 3 rows   (the whole table)
                     analyze { limit: 0 } ->          select * from `orders`      <- no LIMIT at all
PROBE memory-driver  find    { limit: 0 } -> 3 rows
                     find    { limit: 0, offset: 1 } -> 2 rows   <- OFFSET applied, LIMIT not

premise_still_valid: true, on both halves.

One standard, three mechanisms

driver what was wrong the fix
driver-memory find() sliced with if (query.limit) — truthiness, 0 is falsy, so the slice was dropped and a request for nothing got all 12 rows. The { limit: 0, offset: 1 } -> 2 reading is why every paging suite stayed green over it: the OFFSET applied and the LIMIT silently did not. Presence. Plus the two same-shape sites in memory-analytics.ts ($limit stage, SQL string builder). Mingo honours { $limit: 0 } as zero records (measured: 3 in, 0 out), so no short-circuit is needed there.
driver-mongodb Nothing was dropped — buildFindOptions already tested presence, so 0 arrived exactly as written. The divergence is one layer lower: the MongoDB Node driver defines limit: 0 as no limit. Forwarding faithfully was the bug. An explicit short-circuit answering the contract before the client is consulted[] from find, null from findOne, which had the same hole. No round trip for a query whose answer is known, and no future change in the upstream driver's reading of 0 can move it. Deliberately === 0, not <= 0: a negative limit is not a shape the contract defines, and folding it in would invent an answer the validation layer owns.
driver-sql Two doors disagreed with a third. findRows() (what find() uses) always compiled on presence; findWithWindowFunctions() (#4286 — returns rows) and analyzeQuery()/explain() (returns a plan) compiled on truthiness. The plan door explained select * from "orders" where find() sent ... order by "id" asc limit ? — an EXPLAIN for a different statement. Presence at both.
driver-turso (remote) Found by this PR's own conformance control, and a separate defect: OFFSET and LIMIT were emitted independently, but SQLite's grammar is LIMIT expr [OFFSET expr]. So find(obj, { offset: N }) with no limit produced near "OFFSET": syntax error — for every N, not a boundary value, and only on remote (local goes through knex). Emit knex's own LIMIT -1 no-limit sentinel, so both transports build the same statement. Measured: knex compiles .offset(3) to limit ? offset ? bound [-1, 3].

Result sets only ever get narrower. A caller who wants every row should omit limit rather than pass 0.

offset moved with limit in driver-sql — and the honest reason

Both driver-sql doors also spelled if (query.offset). That flip is measured to change nothing: knex elides a zero offset on better-sqlite3, Postgres and MySQL alike (compiled .offset(0) is select * from "orders" on all three). It is made for internal consistency and pinned as the no-op it is, not sold as a fix. My first draft of the changeset claimed the statement changed; the measurement said otherwise and the claim was corrected.

driver-memory's offset is deliberately left on truthiness: slice(0) is the identity slice, so presence and truthiness cannot be told apart there. No behaviour to fix, and the exception was scoped to the limit door.

Reverse verification — direction predicted BEFORE each run

Each experiment reverts one arm and predicts a disjoint red set.

experiment predicted measured
revert both sql-driver.ts doors 3 window-function limit: 0 cases + 2 analyzeQuery LIMIT cases red; all 8 controls green ✅ exactly 5 failed, 8 passed
revert memory-driver.ts slice exactly the 3 zero cases red; the 3 controls and all paging cases green ✅ exactly 3 failed, 19 passed
revert the mongodb-driver.ts guard the 3 guard cases red — and failing on a connection error, not a row count, since the URI is unreachable by construction ✅ exactly 3 failed, 6 passed; the 2 controls that assert the client is consulted stayed green
turso remote, before the sentinel the bare-offset control red with a SQL syntax error SqliteError: near "OFFSET": syntax error

Conformance — pinned across all five drivers, no DEBT

New shared case-set PAGINATION_ZERO_LIMIT_CASES in packages/spec/src/data/pagination-conformance.ts, classified in check-driver-conformance.mjs.

A separate marker from PAGINATION_CASES rather than extra rows in it, deliberately: those cases are about a page being a partition, these are about the page size being read at all, and they fail independently. Keeping them apart is also what keeps the ledger honest — a driver answering one and not the other is a half-covered cell, and a shared marker would let it import its way to green.

Three of the six cases are controls (limit: 2 → 2, bare offset: 0 → 12, no limit → 12). Without them a driver returning [] for everything would pass every zero case and fail its users completely — and for MongoDB specifically, the controls are what stop the new short-circuit from being an unconditional "return nothing".

driver              PAGINATION_ZERO_LIMIT
driver-memory       ok
driver-mongodb      ok
driver-sql          ok
driver-sqlite-wasm  ok
driver-turso        ok        (both transports)
check-driver-conformance: OK — 33 covered cell(s), 2 in the DEBT ledger, 0 exempt.

The 2 surviving DEBT rows are the untouched FILTER_TEXT_CASES pair pointing at #6682 — frozen, and not this PR's business.

Which cells executed, and which did not

  • sqlite / sqlite-wasm / turso (both transports) / memory — ran locally and in CI.
  • live Postgres + MySQLRAN, in CI on this PR. They are not provisionable in the authoring container, where the driver-sql pagination matrix declares them as named skips (declareUnprovisionedCell) rather than faking green — but CI's Temporal Conformance (live PG + MySQL) job sets both URLs and runs this package against both servers. That job is green on 12c8176, so the six new limit: 0 cases executed against live PG and MySQL, not only against embedded SQLite.
  • live mongodNOT run. mongodb-memory-server could not fetch its binary (download blocked in this container), and CI's matrix does not provision one either; it is a declared skip by name, and the 19 live cases stayed skipped rather than reported green. The guard is still proved unconditionally: its server-free block runs on a driver that is never connected, against an unreachable URI by construction — so if the short-circuit were removed those cases would fail on a connection error, which is exactly the right alarm. For this particular contract that is the stronger instrument, since the claim is "no client is consulted".

Gates — enumerated from .github/workflows/lint.yml, run one by one

All 54 check:* steps in lint.yml, run individually: all OK. Plus pnpm lint OK, turbo run typecheck OK (120/120), pnpm --filter @objectstack/spec exec tsc --noEmit OK, check:empty-changeset OK, check:adr-0087-registration OK (no declared-breaking changeset).

Two notes on gates that needed care:

  • check:query-options-erasure went red twice, both times mine: the new test call sites had picked up as any on the query argument (263 → 267, then → 270). Fixed by typing the call sites rather than by raising the ceiling — now back at 263, "no files added".
  • check:i18n / check:i18n-coverage / check:app-nav-i18n / check:type-check-debt fail on an unbuilt tree and say so explicitly ("Nothing was measured"). Green after turbo run build, exactly as lint.yml orders them.

pnpm test (whole repo, locally): one red, plugin-email's attachment-queue test, which timed out at 5s under 120-package parallel load (that run reported transform 74s / import 218s). Re-run alone: 302/302 pass. Unrelated to this diff — no driver, pagination or spec surface in it — and CI's sharded Test Core (1–3/3) is green, which is the authority over my single-box run.

CI on 12c8176: all 25 checks green, mergeable_state: clean.

Changeset

patch on @objectstack/driver-sql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-turso; minor on @objectstack/spec (additive export — no existing export moved). check:adr-0087-registration confirms no declared surface moves: limit still exists and still takes a number; what changed is that the value is now honoured.

Constraints honoured

Out-of-scope finding


Generated by Claude Code

claude added 5 commits August 8, 2026 14:31
…w-function and explain doors (#6577)

`findRows()` has always compiled `limit` on presence (`query.limit !== undefined`),
which is what `limit: 0` — ruled in #6485 to mean "return no records" — depends on.
Two other doors in the same file compiled it on truthiness, and 0 is falsy:

  findWithWindowFunctions()  returns ROWS  -> { limit: 0 } returned the whole table
  analyzeQuery() / explain() returns a PLAN -> explained a statement without the LIMIT

Measured on 3172831 before the change: three rows seeded, find({limit:0}) -> 0 rows
while findWithWindowFunctions({limit:0}) -> 3, and analyzeQuery emitted
`select * from \`orders\`` where find() sent `... order by \`id\` asc limit ?`.

`offset` moved with `limit` for internal consistency only: knex elides a zero
offset on better-sqlite3, pg and mysql2 alike, so that half is measured to change
no statement and no row set. Pinned as the no-op it is.
…emote bare-offset syntax error (#6577)

Adds PAGINATION_ZERO_LIMIT_CASES — the shared case-set pinning #6485's ruling
that `limit: 0` returns no records — with controls so "return nothing, always"
cannot pass it. Answered by driver-sql, driver-sqlite-wasm and driver-turso on
BOTH transports. driver-memory and driver-mongodb take honest DEBT rows: both
are #5499-frozen and diverge for two different reasons (memory drops the slice
on truthiness, measured 3-of-3; mongodb forwards 0 to a client that defines it
as no-limit).

The bare-offset control surfaced a separate live defect in turso's remote
transport: it emitted LIMIT and OFFSET independently, so `{ offset: N }` with
no limit assembled `... OFFSET ?` and SQLite answered `near "OFFSET": syntax
error` — for every N, and only on remote (knex synthesises LIMIT -1 locally).
Remote now builds the statement knex builds.
… options typed (#6577)

- turso remote: the LIMIT -1 sentinel pins now read the SELECT off a recording
  client instead of calling the private `buildSelectSQL`, so they assert what
  the transport actually sent and survive `find()` changing how it builds it.
  Adds the third direction: neither limit nor offset given emits no LIMIT, so
  the sentinel is not unconditional.
- drop the `as any` casts the new call sites had picked up, keeping
  check:query-options-erasure at its 263 ceiling rather than raising it.
… full card)

Per the maintainer's #5499 freeze exception (ruling on #6577: 「A+ 批准,按这个范围执行」),
scoped to the limit door only:

- driver-memory: find() sliced on truthiness, so limit 0 returned the whole
  table (measured 3-of-3; with offset 1 it returned 2, the offset applying and
  the limit not). Presence now. Same shape fixed at the two memory-analytics
  sites; mingo honours $limit 0 as zero records (measured), so no
  short-circuit is needed there.
- driver-mongodb: presence was already correct — the value was forwarded
  faithfully to a client that DEFINES limit 0 as 'no limit'. Answered before
  the client is consulted instead: [] from find, null from findOne. No round
  trip, and the upstream driver's reading of 0 can no longer decide the
  contract.
- Conformance: PAGINATION_ZERO_LIMIT_CASES now answered by all five drivers,
  no DEBT rows. 33 covered, 2 DEBT (the untouched #6682 FILTER_TEXT rows).

The #5499 freeze remains in force for everything else in both packages.
Drops the `as any` the memory/mongodb limit-0 blocks had picked up, holding
check:query-options-erasure at its 263 ceiling instead of raising it to 270.
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 5:50pm

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec.

118 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/driver-turso, @objectstack/spec)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/driver-memory)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/driver-memory)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/driver-memory, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[drivers] InMemoryDriver.find applies limit on truthiness, so limit: 0 returns every row — the #6485 defect one layer below the client

2 participants