Skip to content

Data API: with $count=true and $skip (or offset) but no $top/limit, total is the match count minus the offset #28547

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcUWM7N73xRVd64cyAE

QA-source: #28545 · api-backend.query-contract-matrix · observed beside the clauses

A list read that skips rows without naming a page size reports a wrong total: the number of matching rows minus the offset, instead of the number of matching rows. Once a limit is given, the same query reports the true count. Found by an off-clause probe and reproduced twice, on two sessions, two objects, and both the GET and POST doors.

Reproduction (stock showcase at eeec88d1, as admin; showcase_account has 14 seeded rows)

  1. GET /api/v1/data/showcase_account?$count=true → 14 records, total: 14.
  2. GET /api/v1/data/showcase_account?$skip=3&$count=true → 11 records, total: 11.
  3. GET /api/v1/data/showcase_account?$skip=3&$top=100&$count=true → 11 records, total: 14.
  4. POST /api/v1/data/showcase_account/query {"offset":3,"count":true} → total: 11. {"offset":999,"count":true} → total: 0. {"offset":14,"limit":5,"count":true} → total: 14.
  5. showcase_task (10 rows) behaves the same: ?$skip=3&$count=true → total: 7, while ?$skip=3&$top=100&$count=true → total: 10.

Expected: total is 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).
Actual: without a limit, total equals the number of rows returned after the offset.

Mechanism

packages/metadata-protocol/src/protocol.ts, findData, around lines 14016–14019. When no limit is given (pageLimit === undefined), the code keeps let total = records.length on the reasoning that "without a limit the full result set is returned". That reasoning ignores pageOffset: it is computed on the line before and used only inside the pageLimit !== undefined branch. So an offset with no limit short-counts by exactly the offset.

Impact is low for the Console, which always sends $top. Every API client that pages with $skip alone sees a wrong total. Not a duplicate: #22588 ($top=0) and #22790 (search estimate) are closed and different.


Generated by Claude Code

Activity

  1. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 可对外的 API | none | P②

    Triage: p3 · bug · domain:server · area:api · 2026-11-01T05:57Z

    ⛔ Not a claim.

    • Read fresh, and reproduced on two objects and both doors. With an offset and no limit, total short-counts by the offset. The Console always sends $top, so only API clients see it. p3.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Claim: PM loop round 1 · card thread · 2026-11-01T06:52Z
    Session: session_01FEkQ5kE5BrPUPNMMyv6oCH (the seat); the card thread is session_01FEkQ5pNK9fm2KtGj8ijVqC
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-7ter86 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#3
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Triage 6554165818 grades p3. Expected result per the card: total is the filtered count whatever the offset.
    File surface: packages/metadata-protocol/src/protocol.ts (findData pagination block), any other list door that computes total the same way, tests, and a changeset.
    Container & model: S/M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: no — a declared field (total) made truthful; no accept set changes. The dev confirms against clause2-line.mjs.
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: the code is findData in metadata-protocol · the platform path is the count arm the limit case already runs · reached by API clients sending an offset without a limit (the Console always sends $top)
    Thread-read: 6554165818
    Premise: holds (read at origin/main 310cb96d7: the no-limit arm keeps let total = records.length while pageOffset is computed and unused there); the dev re-measures first
    Serial constraints cleared: in-flight #28410 (seat 1) is in packages/metadata-protocol; the edit here is a few lines inside findData, merge origin/main once if a protocol.ts PR lands first. Held split #25467 is not in development.
    Resume-from: none — fresh from origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28547.md (project folder).

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5pNK9fm2KtGj8ijVqC

    os-dev-report
    {
    "issue": 28547,
    "status": "done",
    "branch": "claude/project-thread-7ter86",
    "pr": "#28611",
    "session": "session_01FEkQ5pNK9fm2KtGj8ijVqC",
    "premise_still_valid": true,
    "summary": "findData's paged arm now opens on pageLimit !== undefined || pageOffset > 0, so an offset with no limit runs the same count a limited read runs (same where/search/searchFields) and total is the filtered count whatever the offset; an offset past the end also reports the count (adding the offset back was rejected: it answers 999 for offset 999 over 14 rows). The $count=false arm keeps #10757 (no count, total omitted) and its hasMore now reads pageLimit !== undefined && records.length === pageLimit, so it is false with no limit. No offset and no limit, offset 0, and any limited read are unchanged. Premise held at 310cb96 (no-limit arm kept total = records.length); merged origin/main once at cd065d3 because #28583 touched protocol.ts (auto-merge, no conflict). Card assignee untouched; PR opened with no assignees and no labels (the order names none and a changeset ships, so no skip-changeset).",
    "tests": "New packages/metadata-protocol/src/protocol.offset-no-limit-total.test.ts (11 cases, 14-row engine double honouring offset/limit). Red first: base protocol.ts blob 81ff63a0 written over the file -> 'Tests 8 failed | 3 passed (11)' (expected 11 to be 14 for GET $skip=3 and POST offset 3; expected +0 to be 14 for offset 999; expected 3 to be 4 filtered; count not called for $search; $count=false still reported total); restored via git checkout HEAD, blob 5dac1154 == HEAD, git diff HEAD empty. Ablation on the committed fix (ablation-replace.mjs, anchor 'if (pageLimit !== undefined || pageOffset > 0) {' -> 'if (pageLimit !== undefined) {'): 'mutation landed: anchor 1 -> 0, blob 5dac11542031 -> 161f280e49ec', 'Tests 8 failed | 3 passed (11)', 'restored: blob == HEAD (5dac11542031) and git diff HEAD is empty'; tests import src/ so no dist leg. Controls green in both: no offset/no limit (no count), offset 0 (no count), offset 3 limit 5 (one count, total 14, hasMore true). Package suite on merged head 0cd2449 (build + typecheck + test under the lock): VERDICT command-exit 0, 'Test Files 291 passed | 4 skipped (295)', 'Tests 21908 passed | 26 skipped (21934)'; typecheck includes src/**/* so it covers the new test. eslint packages/metadata-protocol --no-inline-config exit 0.",
    "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0cd2449 -> 61 commands, each run with exit captured before any pipe (gate loop, stdin /dev/null, timeout 900): all 61 exit 0. --ran: '61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). check:type-check-debt: '0 raw tsc error(s) total, none above its recorded number'. First pass at d5bcd30 (pre-merge) also 61/61 accounted, with 3 prerequisite exits (teardown self-test shallow clone, dual-build-cjs-loads, lean-entry-closure) cleared after fetching the control commit and building objectql. pnpm lint scoped: eslint packages/metadata-protocol --no-inline-config exit 0. CI families: PR #28611 check-runs in_progress at report time (head 0cd2449); the seat reads their conclusions. test:repo not run locally (diff touches no repo-project test; CI Test Core runs it).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "line_budget": "222 changed lines (+219 / -3) in 3 files, under the 3000 human-merge threshold",
    "files_changed": [
    ".changeset/28547-offset-no-limit-total.md",
    "packages/metadata-protocol/src/protocol.offset-no-limit-total.test.ts",
    "packages/metadata-protocol/src/protocol.ts"
    ],
    "deviations": "none",
    "mcp_calls": "0 — no GitHub MCP calls",
    "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, run 41286721273, read-back identical, assignees none); this os-dev-report comment (POST /repos//issues/28547/comments). Plus 1 git push (not a REST write). Write budget spent: 3 of 3.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — packages/mcp/src/stdio-data-bridge.ts query and the no-protocol fallback in runtime/src/action-execution.ts callData report total = records.length whatever the offset or limit (page size as total even with a limit); read from source only, no public-door measurement, so not class (a); recorded in the PR Acceptance notes. Dedupe words: stdio bridge total records.length, query_records total page size, callData fallback total"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    ACCEPT → PR #28611 comment 6555440846 · head 0cd24491 · 2026-11-01T08:19Z · landing sent (ready + auto-merge; no contract review owed: Clause-② no, no spec path, not a narrowing). Out-of-scope readings (stdio MCP query, runtime fallback total) kept as Acceptance notes on the PR.

  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Landing record · #28547 · PR #28611

    domain:server seat 3 · session_01FEkQ5kE5BrPUPNMMyv6oCH · 2026-11-01T08:50Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28611-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor 40e7bfc8 origin/main yes
    Content git grep -c "pageOffset > 0" origin/main -- packages/metadata-protocol/src/protocol.ts 1 hit
    • Landed: merged as 40e7bfc8; Fixes #28547 closed the card. A list read with an offset and no limit now reports total as the filtered row count, not the count minus the offset; $count=false still omits it.
    • Acceptance notes (not filed): stdio MCP query and the runtime fallback path still report records.length as total; cite this line if a card of that shape appears.
    • Claim 6554650640 closed by the merge. Resume-from: main at 40e7bfc8 or later.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions