Package su.onno.query

Class Keyset

java.lang.Object
su.onno.query.Keyset

public final class Keyset extends Object
The SQL behind keyset (a.k.a. "seek") pagination — the constant-time alternative to LIMIT .. OFFSET ... Instead of counting past offset rows the database discards, the next page is found by seeking straight to the row after a cursor with an indexed comparison, so fetching page 1 and page 10 000 cost the same. The _id tiebreaker makes the order total, which also fixes offset paging's skip/duplicate bug when rows shift between fetches.

Two shapes, chosen by whether the sort column can be null:

  • Non-null fast pathORDER BY col DIR, _id DIR with a plain (col, _id) seek. A composite (col, _id) index serves it as an index-only range scan in either direction. This is the hot path for the default sorts (_code, _date, _number — all non-null).
  • NULL-safe path — for a nullable attribute sort, ordering is forced NULLS-LAST with a leading (col IS NULL) rank and the seek branches on whether the cursor row sat in the null tail, so nulls are never skipped or duplicated. Correct, but the leading rank expression means the composite index can't fully serve the ordering — acceptable for the rarer nullable-column sort.

The builder only renders SQL; the caller binds VALUE_BIND / ID_BIND from the cursor (guarded by Keyset.Plan.bindsValue()), keeping value handling parameterised and safe.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final record 
    The rendered ORDER BY body and seek predicate for one page.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final int
    Default page size when a client doesn't ask for one — small, since keyset favours many cheap pages.
    static final String
     
    static final int
    Hard ceiling so one request can't pull an unbounded window (mirrors the list API's cap).
    static final String
    Reserved bind names for the seek comparison — distinctive so they never collide with a column filter.
  • Method Summary

    Modifier and Type
    Method
    Description
    static int
    clampLimit(int limit)
    Clamp a requested page size into [1, MAX_LIMIT], defaulting a non-positive request.
    plan(String sortColumn, boolean descending, boolean nullable, Cursor cursor)
    Build the page plan for sortColumn in the given direction, seeking past cursor (null for the first page).

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • DEFAULT_LIMIT

      public static final int DEFAULT_LIMIT
      Default page size when a client doesn't ask for one — small, since keyset favours many cheap pages.
      See Also:
    • MAX_LIMIT

      public static final int MAX_LIMIT
      Hard ceiling so one request can't pull an unbounded window (mirrors the list API's cap).
      See Also:
    • VALUE_BIND

      public static final String VALUE_BIND
      Reserved bind names for the seek comparison — distinctive so they never collide with a column filter.
      See Also:
    • ID_BIND

      public static final String ID_BIND
      See Also:
  • Method Details

    • clampLimit

      public static int clampLimit(int limit)
      Clamp a requested page size into [1, MAX_LIMIT], defaulting a non-positive request.
    • plan

      public static Keyset.Plan plan(String sortColumn, boolean descending, boolean nullable, Cursor cursor)
      Build the page plan for sortColumn in the given direction, seeking past cursor (null for the first page). nullable selects the NULL-safe shape.