page: transport-neutral keyset pagination for Bob #6

Closed
opened 2026-10-26 22:16:24 -07:00 by end · 1 comment
Owner

Root-module package go.auvem.com/corestack/page. The connect adapter, and gql if it's ever built, sit on top of this package. Keep it free of GraphQL and protobuf types.

Clean-room rewrite. Build from this spec. Don't read or port go-toolkit/cursor or go-toolkit/dbx.

Requirements

  • Sorts. A sort is an ordered list of columns with directions; mixed directions are allowed. It must end in a unique tie-breaker column, and sorts without one are rejected.
  • Opaque cursors.
    • A cursor is a versioned encoding of the sort-column values, in base64url.
    • Decoding errors, version mismatches and sort mismatches return typed errors.
    • Cursors aren't a security boundary: values are only ever bound parameters, and authorization stays in the query.
  • Composable query mods. The keyset WHERE and ORDER BY are applied to a Bob query as query mods, so callers keep adding their own dynamic filters.
  • Both directions: forward (first/after) and backward (last/before).
  • Page size. Fetch limit+1 to determine has-next and has-previous. Clamp the page size to a configured default and maximum.
  • Page[T] holds the items, the start and end cursors, and has-next/has-previous.
  • Total count is a separate, opt-in function, because counts are expensive.
  • Non-null columns. v0 requires non-null sort columns; document this.
  • Dialects. Postgres is the primary target. MySQL and SQLite should work through Bob's dialects; report any dialect you couldn't support.

Done when

  • Tests cover:
    • encoding round-trips;
    • tampered and mismatched cursors;
    • multi-column sorts with mixed directions and ties;
    • both directions;
    • page boundaries;
    • page-size clamping.
  • Tests run against SQLite in-process and against Postgres in CI, using a service container.
  • The cursor encoding format is recorded as an ADR, because adapters and stored cursors depend on it.
Kickoff prompt
Use the feature-delivery skill to resolve corestack/go#6
(read the issue body; it is the spec). Repo ~/Projects/corestack/go, base main,
branch feat/page. Read AGENTS.md and the accepted ADRs first. Clean-room: do not read or
port go-toolkit/cursor or go-toolkit/dbx. If Bob's current query-mod API makes part of
the spec awkward, report it instead of working around it. Independent review before the PR.
Root-module package `go.auvem.com/corestack/page`. The `connect` adapter, and `gql` if it's ever built, sit on top of this package. Keep it free of GraphQL and protobuf types. **Clean-room rewrite.** Build from this spec. Don't read or port `go-toolkit/cursor` or `go-toolkit/dbx`. ## Requirements - **Sorts.** A sort is an ordered list of columns with directions; mixed directions are allowed. It must end in a unique tie-breaker column, and sorts without one are rejected. - **Opaque cursors.** - A cursor is a versioned encoding of the sort-column values, in base64url. - Decoding errors, version mismatches and sort mismatches return typed errors. - Cursors aren't a security boundary: values are only ever bound parameters, and authorization stays in the query. - **Composable query mods.** The keyset WHERE and ORDER BY are applied to a Bob query as query mods, so callers keep adding their own dynamic filters. - **Both directions:** forward (first/after) and backward (last/before). - **Page size.** Fetch limit+1 to determine has-next and has-previous. Clamp the page size to a configured default and maximum. - **`Page[T]`** holds the items, the start and end cursors, and has-next/has-previous. - **Total count** is a separate, opt-in function, because counts are expensive. - **Non-null columns.** v0 requires non-null sort columns; document this. - **Dialects.** Postgres is the primary target. MySQL and SQLite should work through Bob's dialects; report any dialect you couldn't support. ## Done when - Tests cover: - encoding round-trips; - tampered and mismatched cursors; - multi-column sorts with mixed directions and ties; - both directions; - page boundaries; - page-size clamping. - Tests run against SQLite in-process and against Postgres in CI, using a service container. - The cursor encoding format is recorded as an ADR, because adapters and stored cursors depend on it. <details> <summary>Kickoff prompt</summary> ```text Use the feature-delivery skill to resolve corestack/go#6 (read the issue body; it is the spec). Repo ~/Projects/corestack/go, base main, branch feat/page. Read AGENTS.md and the accepted ADRs first. Clean-room: do not read or port go-toolkit/cursor or go-toolkit/dbx. If Bob's current query-mod API makes part of the spec awkward, report it instead of working around it. Independent review before the PR. ``` </details>
end added a new dependency 2026-10-26 22:17:23 -07:00
Author
Owner

Spec clarification (2026-10-27): getting cursor values from a row

The original spec didn't say how page gets the sort-column values from a row to build a cursor. This clarification is part of the spec, and it was sent to the in-flight delivery as a follow-up.

  • The caller passes a typed key function alongside the Sort, for example func(T) []any. It returns the sort-column values in Sort order.
  • No reflection, no struct tags, and no lookup of columns by name on T.
  • Validate the length at runtime. The key function must return exactly len(Sort) values. A mismatch is a programmer error and gets a clear message, never a silently bad cursor.
  • Supported types. Cursor encoding round-trips these values for the column types supported in v0: ints, strings, time.Time and UUIDs at minimum. Document the supported set, and reject other types with a typed error.
  • Keep the pieces plain. The Sort definition and the key function stay as two plain values the caller writes for each query. Codegen may generate them later (#11), so don't hide them behind an abstraction now.

Done when (in addition to the existing criteria):

  • a test covers a key function that returns the wrong number of values for the Sort;
  • the package Example shows the key function.
## Spec clarification (2026-10-27): getting cursor values from a row The original spec didn't say how `page` gets the sort-column values from a row to build a cursor. This clarification is part of the spec, and it was sent to the in-flight delivery as a follow-up. - **The caller passes a typed key function** alongside the `Sort`, for example `func(T) []any`. It returns the sort-column values in `Sort` order. - **No reflection**, no struct tags, and no lookup of columns by name on `T`. - **Validate the length at runtime.** The key function must return exactly `len(Sort)` values. A mismatch is a programmer error and gets a clear message, never a silently bad cursor. - **Supported types.** Cursor encoding round-trips these values for the column types supported in v0: ints, strings, `time.Time` and UUIDs at minimum. Document the supported set, and reject other types with a typed error. - **Keep the pieces plain.** The `Sort` definition and the key function stay as two plain values the caller writes for each query. Codegen may generate them later (#11), so don't hide them behind an abstraction now. **Done when** (in addition to the existing criteria): - a test covers a key function that returns the wrong number of values for the `Sort`; - the package `Example` shows the key function.
end closed this issue 2026-10-27 20:21:59 -07:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Reference: corestack/go#6