Skip to content

fix(data): paging a sorted read is a partition of the result set, not five queries that share a WHERE clause (objectui#3106) - #4367

Merged
os-zhuang merged 1 commit into
mainfrom
claude/data-table-pagination-sort-733f71
Jul 31, 2026
Merged

fix(data): paging a sorted read is a partition of the result set, not five queries that share a WHERE clause (objectui#3106)#4367
os-zhuang merged 1 commit into
mainfrom
claude/data-table-pagination-sort-733f71

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

ORDER BY status LIMIT 50 OFFSET 50 names a sort key that does not identify a row. No backend promises that rows with equal keys keep the same relative arrangement between two queries — MongoDB documents the opposite: sort + skip/limit on a non-unique key may return the same document more than once.

So paging a sorted list was never a partition of the collection:

page 1: ORDER BY status LIMIT 5 OFFSET 0   -> [… r04 …]
page 2: ORDER BY status LIMIT 5 OFFSET 5   -> [… r04 …]     r04 twice; one row never served at all

Every page is full. Every row is real and belongs. The duplicate sits several screens from the omission — which is why this is found by a user counting records, never by reading a response.

How

SqlDriver and MongoDBDriver append a unique tie-breaker to any non-empty orderBy, in the last requested key's direction. Determinism holds either way, but a same-direction suffix is the one a compound index can still walk in a single pass.

Two deliberate restrictions:

  • SqlDriver applies it only to objects it created itself (initObjects records those in managedObjectFields). A federated table (ADR-0015) may carry no id column, and guessing there is worse than doing nothing: the unknown-column error is answered by fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821's recovery ladder retrying with no ORDER BY at all — trading a reshuffle among ties for the loss of the caller's whole sort.
  • driver-memory needed no change. Array#sort is stable and the backing table's order does not move between reads, so it already conformed. It gets a suite anyway: that guarantee is implicit, and a refactor that looks like a speed-up (a hand-rolled sort, or sorting the array in place) would remove it without failing any other test in the package.

MongoDBDriver's two sort sites (find and _findStream) were identical copies; they now share one buildSortSpec, so the tie-breaker cannot be added to one and forgotten in the other.

The contract

The obligation is now normative on IDataDriver.find, and the cases are sharedPAGINATION_CASES in @objectstack/spec/data, following the TEMPORAL_CASES precedent. A future driver is held to this by a gate rather than by remembering it.

Each driver runs the shared cases; driver-sql and driver-mongodb additionally assert the emitted clause. That second half matters here: SQLite over a twelve-row table returns ties in rowid order every time, so the property test alone would pass the day someone deletes the feature. On MongoDB the property test is the real one.

Tests

suite result
driver-sql (full) 603 passed, 38 skipped
driver-memory (full) 275 passed
driver-mongodb (full, real MongoDB via mongodb-memory-server) 173 passed
pnpm --filter @objectstack/spec check:generated all 8 artifacts up to date (api-surface.json regenerated for the 2 new exports)

Out of scope

A paged read with no orderBy at all is non-deterministic on every backend by definition, and imposing an order on callers who asked for none changes plan selection far more broadly than this does. Filed as #4363 per Prime Directive #10 rather than folded in here.

Context

This is the server-side half of objectui#3106 (column-header sort under server pagination). The client half lands in objectui; that work makes low-cardinality columns like status the most common sort key, which turns this from a latent property violation into a daily one.

🤖 Generated with Claude Code

… five queries that share a WHERE clause (objectui#3106)

`ORDER BY status LIMIT 50 OFFSET 50` names a sort key that does not identify a
row, and no backend promises that rows with equal keys keep the same relative
arrangement between two queries. MongoDB documents this outright: `sort` +
`skip`/`limit` on a non-unique key may return the same document more than once.
Page 2 then repeats a row page 1 already showed and skips one nobody ever sees —
with every page full, every row real, and the two halves of the symptom several
screens apart.

SqlDriver and MongoDBDriver append a unique tie-breaker to any non-empty
`orderBy`, in the last requested key's direction: determinism holds either way,
but a same-direction suffix is the one an index can still walk in a single pass.

SqlDriver applies it only to objects it created itself (`initObjects` records
those in `managedObjectFields`). A federated table (ADR-0015) may carry no `id`
column, and guessing there would be worse than doing nothing — the resulting
unknown-column error is answered by #3821's recovery ladder retrying with NO
ORDER BY at all, trading a reshuffle among ties for the loss of the caller's
whole sort.

driver-memory needed no change: `Array#sort` is stable and the backing table's
order does not move between reads. It gets a suite anyway, because that
guarantee is implicit and is exactly what a refactor that looks like a speed-up
(a hand-rolled sort, or sorting the array in place) would silently remove.

The obligation is normative on `IDataDriver.find` and the cases are shared
(`PAGINATION_CASES` in `@objectstack/spec/data`), so a future driver is held to
it by a gate rather than by remembering. A paged read with NO `orderBy` is
deliberately out of scope and filed as #4363.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 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 Jul 31, 2026 9:08am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/l labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

109 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 packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-mongodb, @objectstack/driver-sql, @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/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • 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-mongodb, @objectstack/driver-sql)
  • 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 packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/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 packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • 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/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-mongodb, @objectstack/driver-sql, @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/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/kernel/runtime-capabilities.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-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-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/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.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.

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.

1 participant