feat(gql): replace cursor pagination with openCypher SKIP/LIMIT

What does this MR do and why?

Orbit queries page the way Cypher does: SKIP n LIMIT m, with OFFSET as a synonym. Keyset cursor pagination (PAGE n AFTER 'token') is removed. Agents already know SKIP from Cypher and kept stumbling over opaque tokens bound to the exact query text. Pages stay stable because any query with SKIP gets a deterministic sort.

Breaking: PAGE/AFTER, the JSON DSL cursor, and next_cursor in responses are gone. Plain LIMIT behaves as before.

Relates to !2818, which removes the JSON DSL. Analytics: gitlab-org/iglu!218 adds a skip field to orbit_query (2-2-2); a follow-up pins it once it merges. Part of #1258, #1311

Testing

  • Full local suite on the rebased branch: unit (2,506), local and plan-shape (231), containers including data correctness and corpus smoke (218), CLI (58), and code graph (646). fmt, clippy, dashboards, version pins, and skill checks are clean.
  • End to end on a local Caproni stack: this branch's webserver, indexer, and dispatcher behind real Rails (19.5), queried through the REST API as an admin and as a Reporter. SKIP pages covered every row with no repeats across ordered, unordered, relationship, neighbors, and aggregation queries, with correct has_more. Pages for the Reporter matched the Rails-visible set exactly.
  • Rerun after rebasing onto main at 900729a1, with this branch's webserver on Caproni, as root and as a Reporter on the indexed gitlab-org/cli import. Stepping SKIP returned every row exactly once and matched the unpaged result, up to 1,663 rows or 22 pages per query. That held for unordered, ordered, tied-sort, relationship, multi-hop, and aggregation queries, with has_more false only on the last page. The Reporter's pages matched its visible set: 13 of 17 work items. SKIP near the end of 7,613 definitions, SKIP 10000, OFFSET, and SKIP without LIMIT work. PAGE, AFTER, LIMIT before SKIP, SKIP 10001, negative SKIP, and LIMIT 1001 return 400s. cargo check and clippy pass on the rebased branch.

GQL output, before and after:

before: 5 rows, more available
        next_cursor: "eyJoIjoiM..."
after:  5 rows, more available

Performance Analysis

  • This merge request does not introduce any performance regression. If a performance regression is expected, explain why.

Queries without SKIP compile to the same SQL as before. A query with SKIP sorts by the compiler's tie-breakers and reads the skipped rows plus the page, so cost grows with the offset. SKIP is capped at 10,000.

Agent context: long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered

Language. [ORDER BY …] [SKIP n] [LIMIT m] [DEBUG]. SKIP must precede LIMIT, as in openCypher. SKIP takes 0 to 10,000 and LIMIT 1 to 1,000. OFFSET is now a reserved word; page and after are not.

Ordering. Any SKIP, including SKIP 0, extends ORDER BY with the plan's stable tie-breakers (node identities, edge keys, path arrays, group keys), so consecutive windows neither overlap nor drop rows. The docs tell agents to write SKIP on every page, starting at 0.

Visibility. A page can return fewer rows than LIMIT when some rows are not visible to the caller. SKIP counts by position, so pages shrink instead of shifting. The old cursor anchored on the last visible row and could re-scan rows at page boundaries, so one scenario's per-page split changed (pagination/redaction_multi_hop). The combined result is identical.

Compiler. The cursor pass becomes a page pass (page.rs) that keeps the +1 probe row for has_more and lowers SKIP to OFFSET. Query hashing, cursor tokens, PaginationContext, and the pagination rejection reason are removed. Text excerpts are sized by the rows the final sort reads (skip plus window) rather than the rows returned, so deep pages get shorter excerpts.

Contracts. query_dsl 13.0.0, raw_output_format 6.0.0, toon_output_format 3.0.0, gql_output_format 2.0.0, with matching schema $id majors. Rails and glab do not read these versions. The JSON DSL no longer accepts cursor and has no SKIP; it is being removed in !2818. Analytics sends has_cursor false until the iglu pin lands.

Tests. Cursor-specific Rust tests are removed; coverage moved to YAML scenarios. The scenario runner pages GQL by stepping SKIP, and pagination scenarios are GQL-only. New scenarios cover SKIP SQL shape, OFFSET, SKIP past the end, SKIP without LIMIT, bounds, clause order, and excerpt sizing.

Docs. Query language reference, Orbit skill references (skill 0.33.15), the e2e robot test, and the query corpus.

Commits:

  • 99cac0ef feat(gql): replace cursor pagination with openCypher SKIP/LIMIT
  • 9e113721 fix(gql): follow-ups for SKIP/LIMIT pagination
  • 01fb35d4 chore(compiler): drop the unused base64 dependency
  • 74137113 docs(gql): document key-based paging past the SKIP cap
  • cbb7f191 test(query): use LIMIT in prod_perf pagination corpus queries
Edited by Michael Usachenko

Merge request reports

Loading
Loading