Repository navigation
fix(metadata-protocol): an offset with no limit reports the filtered total - #28611
Conversation
…total `findData` treated a request as paged only when it carried a limit. Without one it kept `total = records.length`, which is the match count minus the offset once an offset is given (`?$skip=3&$count=true` over 14 rows answered `total: 11`, and an offset past the end answered `total: 0`). An offset with no limit now takes the same count path as a paged request, `$count=false` arm included. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pNK9fm2KtGj8ijVqC
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pNK9fm2KtGj8ijVqC
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4ce493f53bc92309db7285697bde87f5d03626b5 && git checkout 4ce493f53bc92309db7285697bde87f5d03626b5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d94dd3a65d8ebdf2f5c7084591efaafb1a1f7fdb 0cd24491da48c6ef5aa53f4e67f8bfbed3631e92 && git checkout -B drift-repro d94dd3a65d8ebdf2f5c7084591efaafb1a1f7fdb && git merge --no-ff 0cd24491da48c6ef5aa53f4e67f8bfbed3631e92
node scripts/docs-audit/affected-docs.mjs --json d94dd3a65d8ebdf2f5c7084591efaafb1a1f7fdb
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28611 · 2026-11-01T08:17ZHead-sha: 维护者速读(席位终稿)
核对六项
|
Requested by Leehom · project thread
Fixes #28547
Clause-②: no
Metadata-protocol: none
维护者速读
$skip/offset)不给条数时,total现在是符合筛选条件的总行数,不再是“总行数减偏移”。findData分页块的一个条件:有偏移无条数的请求走已有的计数路径(多一次 COUNT 查询)。无偏移无条数、有条数、$count=false三种行为不变。回滚即 revert 本 PR。packages/metadata-protocol/src/protocol.ts中if (pageLimit !== undefined || pageOffset > 0)一行及$count=false分支的hasMore;protocol.offset-no-limit-total.test.ts。pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.offset-no-limit-total.test.ts,预期 11 个用例全绿。What changes
findData(packages/metadata-protocol/src/protocol.ts) treated a list read as paged only when it carried a limit. Without one it kepttotal = records.length, on the comment "without a limit the full result set is returned". With an offset that is false: the read returns every match after the offset, sototalshort-counted by exactly the offset.The paged arm now opens on
pageLimit !== undefined || pageOffset > 0. An offset with no limit runs the same count a paged request runs, over the samewhere,searchandsearchFields(#22790). Inside that arm:$countabsent or true: oneengine.count(), andtotalis the filtered count.hasMoreis true only while the offset plus the rows returned falls short of the count, so it is false when every row after the offset came back.$count=false: no count is issued andtotalis omitted (Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757).hasMorenow readspageLimit !== undefined && records.length === pageLimit, so with no limit it is false (every row after the offset came back). Before, the guard was not needed because this arm was reached only with a limit.Adding the offset back to
records.lengthwas measured and rejected: an offset past the end returns no rows, so it would answertotal: 999for{"offset":999}over 14 rows. The count path gives 14.A read with neither an offset nor a limit is unchanged: no count,
totalis the set length. An explicitoffset: 0is no offset (pageOffsetis computed as> 0).Why
The card's expected result: "
totalis the number of rows matching the filter, whatever the offset, as it already is when a limit is present (and as #22690 / #22588 settled for$top=0)." The Console always sends$top, so API clients paging with$skipalone were the ones reading the wrong number.Tests
New suite
packages/metadata-protocol/src/protocol.offset-no-limit-total.test.ts, on an engine double that honoursoffsetandlimitas written and counts over the samewhere(the shape ofprotocol.zero-limit-total.test.ts), with 14 rows like the showcase'sshowcase_account:GET ?$skip=3&$count=true: 11 rows,total14,hasMorefalse, one count.POST {"offset":3,"count":true}: the same.{"offset":999,"count":true}: no rows,total14.$top=100gives the sametotaland rows.$countwithskip=3:total14.tier=b, 4 rows) with$skip=1gives 3 rows andtotal4, and the count carries thewhere. A$searchis passed to bothfindandcount.offset: 0issues no count;offset:3, limit:5issues one count,total14,hasMoretrue;$count=falsewith an offset (both the GET and the POST spelling) issues no count, omitstotal, returns 11 rows,hasMorefalse.Red first: the suite against the base protocol.ts (310cb96)
The base blob (
81ff63a0) was written over the file and the suite run, then restored withgit checkout HEAD -- …(blob back to5dac1154,git diff HEADempty).The 3 that stay green are the unchanged-arm controls (no offset and no limit,
offset: 0, a limit).Ablation: revert only the arm condition on the committed fix
node scripts/ablation-replace.mjs --file packages/metadata-protocol/src/protocol.ts --anchor 'if (pageLimit !== undefined || pageOffset > 0) {' --replacement 'if (pageLimit !== undefined) {'. The test imports./protocol.jsfromsrc/, so no build is involved.Package suite, typecheck, lint
pnpm --filter @objectstack/metadata-protocol build && … typecheck && … testunder the verify lock:VERDICT command-exit 0,Test Files 290 passed | 4 skipped (294),Tests 21905 passed | 26 skipped (21931). The packagetsconfig.jsonincludessrc/**/*, so typecheck covers the new test file.protocol.zero-limit-total.test.tsandprotocol.count-opt-out.test.tsstay green (27 of 27 with the new suite). The count-opt-out pin "$count=falsewithout a limit is a no-op" has no offset and is unchanged.eslint packages/metadata-protocol --no-inline-config: exit 0.Acceptance notes
Other producers of a list
total, each with a conclusion:GET /data/:objectandPOST /data/:object/query(packages/rest/src/crud-routes.ts:164,:379): both callp.findData. Fixed here.GET /data/:objectandPOST /data/:object/query(runtime/src/domains/data.ts→action-execution.tscallData('query')→protocol.findData,:779): fixed here.findData's aggregate branch (groupBy/aggregations): no offset reachesengine.aggregate, andtotalis the length of the full grouped set. Not affected.query_records(packages/mcp/src/mcp-http-tools.ts:1127, viaruntime/src/domains/mcp.ts:711callData('query')): always sends a limit (Math.min(limit ?? maxLimit, maxLimit)), so it already took the count arm. Already correct, and it now shares the fix if that ever changes.packages/mcp/src/stdio-data-bridge.ts:971(stdio MCPquery) and the no-protocol fallback inruntime/src/action-execution.ts(total: all.length, reached only when the metadata-protocol service is not registered) reportrecords.lengthastotalwhatever the offset or limit. That is a different defect (page size reported as total even with a limit), read from source and not measured at a public door, so it is not filed. Carrier: none.The fix costs one
COUNTper offset-only read, the same cost a limited read already pays. A caller that wants to avoid it sends$count=false.Verification
Derived gate list:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat0cd24491(61 commands, after mergingorigin/mainonce atcd065d36because #28583 touchedprotocol.ts), each run with its exit code captured before any pipe, reconciled with--ran.--ranreconciliation:61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN(RAN_EXIT=0), every family exit 0, includingcheck:type-check-debt("0 raw tsc error(s) total, none above its recorded number"),check:dual-build-cjs-loads,check:lean-entry-closure,check:engine-double-contract,check:query-options-erasure,check:objectql-double-limitandcheck:where-matcher.Test Files 291 passed | 4 skipped (295),Tests 21908 passed | 26 skipped (21934), typecheck exit 0,eslint packages/metadata-protocol --no-inline-configexit 0.d5bcd30c(before the merge) also accounted for all 61; three families answered exit 3 (prerequisite not met in a fresh worktree) and were green after the prerequisite was met.CI runs the full farm (Test Core shards, lint, type-check lanes) on this PR.
Human Minimum
packages/specholds no runtime logic. (No spec change.)pnpm lintpasses (scoped to the touched package locally; CI runs the full lint).main, one pull request..changeset/28547-offset-no-limit-total.md,@objectstack/metadata-protocolpatch), and both declaration lines are filled in here and in the changeset.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Report on the card: the
os-dev-reportcomment fromdomain:server#3 · os-project-manager · session_01FEkQ5pNK9fm2KtGj8ijVqC.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5pNK9fm2KtGj8ijVqC
Generated by Claude Code