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-zhuangos-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-#6706origin/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 3172831before 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

driverwhat was wrongthe fix
driver-memoryfind() 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-mongodbNothing was dropped — buildFindOptions already tested presence, so 0 arrived exactly as written. The divergence is one layer lower: the MongoDB Node driver defineslimit: 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-sqlTwo 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 everyN, 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.

experimentpredictedmeasured
revert both sql-driver.ts doors3 window-function limit: 0 cases + 2 analyzeQuery LIMIT cases red; all 8 controls green✅ exactly 5 failed, 8 passed
revert memory-driver.ts sliceexactly the 3 zero cases red; the 3 controls and all paging cases green✅ exactly 3 failed, 19 passed
revert the mongodb-driver.ts guardthe 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 sentinelthe bare-offset control red with a SQL syntax errorSqliteError: 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 untouchedFILTER_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 54check:* 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

…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

vercelBot commented Aug 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 8, 2026 5:50pm

Request Review

@github-actions

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-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 8, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

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

@os-zhuang@claude