Skip to content

feat!: the empty cursor is the empty page token, both ways - #9

Merged
iamralch merged 1 commit into
mainfrom
feat/empty-cursor
Sep 24, 2026
Merged

iamralch merged 1 commit into
mainfrom
feat/empty-cursor

Conversation

@iamralch

Copy link
Copy Markdown
Contributor

A first page had no cursor, and a last page returned None. So every caller special-cased the empty page_token on the way in and turned None back into it on the way out. It's the same branch the blank filter needed before #8. AIP-158 reads the empty token as the first page in a request, and as "no more pages" in a response.

let cursor = Cursor::parse(&page_token)?;              // "" -> the empty cursor
query.with_cursor(cursor);                             // empty: no cursor at all
let page = pager.next_page(rows)?;
let next_page_token = page.cursor.encode();            // last page: ""
  • Cursor gains a default, the empty cursor (no keys, no position), and is_empty(). Cursor::parse("") (or whitespace) returns it, and encode() of it returns "".
  • with_cursor(empty) is no cursor: nothing is AND-ed in. There's also no cursor ordering to compare an explicit order_by against, so a first page can't trip CursorMismatch. A later empty cursor clears an earlier one, just as a second with_cursor replaces the first.
  • Page::cursor is a plain Cursor, empty on the last page.
  • Pager docs note that over an empty ordering every page is the last, since there's nothing to position by.

Breaking: Page::cursor is Cursor, not Option<Cursor>; check is_empty() for the last page. Cursor::parse("") now returns Ok with the empty cursor instead of a token error.

Tests:

  • the empty token and the empty cursor round-trip;
  • an empty cursor composes exactly like no cursor, even beside an explicit order_by;
  • an empty cursor clears a real one;
  • the SQLite and PostgreSQL pager tests now start from the empty token/cursor and stop on an empty one.

The README's keyset section is updated.

A first page had no cursor and a last page returned `None`, so every
caller special-cased the empty `page_token` on the way in and mapped
`None` to it on the way out -- the same branch the blank filter needed
before #8. AIP-158 reads the empty token as the first page in a request
and as "no more pages" in a response.

`Cursor` gains a default, the empty cursor -- no keys, no position -- and
`is_empty()`. `Cursor::parse("")` returns it and `encode()` of it returns
"", so a token and a cursor are each other's image, empty one included.
`with_cursor` takes the empty cursor as no cursor, which also keeps it
clear of the order_by mismatch check a first page would otherwise fail.
`Page::cursor` is a plain `Cursor`, empty on the last page.

BREAKING CHANGE: `Page::cursor` is `Cursor`, not `Option<Cursor>`: test
`is_empty()` for the last page. `Cursor::parse("")` returns `Ok` with the
empty cursor rather than a token error.
@iamralch iamralch self-assigned this Sep 24, 2026
@iamralch iamralch added the enhancement New feature or request label Sep 24, 2026
@iamralch
iamralch merged commit 9b83709 into main Sep 24, 2026
5 checks passed
@iamralch
iamralch deleted the feat/empty-cursor branch September 24, 2026 09:25
@github-actions github-actions Bot mentioned this pull request Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant