• 
      

    Add base support for keyset pagination.

    Review Request #15274 — Created Sept. 4, 2026 and updated

    Information

    Djblets
    release-7.x

    Reviewers

    Django's database paginators work by tracking what page you're in and
    generating new SQL using OFFSET and LIMIT to select the right page
    of results to return. This has a major performance impact the further in
    you go, since the database often has to fetch the values first before
    calculating the offset into it. With heavy usage and deep lists of
    pages, this gets expensive fast.

    Keyset pagination is an alternative approach that replaces page-based
    offsets with "cursors". A cursor is a representation of a filter query
    that selects all entries before or after the current page of results.
    This involves selecting using sortable criteria (such as a timestamp)
    along with a "PK tie-breaker" (using a sortable row ID as the last
    criteria in case the rows otherwise match), and a query that's sorted in
    the same direction.

    With this in place, the paginator can encode a cursor that says (for
    example), "Select results after this last result's timestamp and PK."
    Or when traversing backwards, "Select results before his first result's
    timestamp and PK."

    There are some tradeoffs. You can't jump to arbitrary pages (though
    there's a trick here) and you can't effectively get the full result
    count (as that can have the same expense that pagination has for
    filtered results). This usually leads to UIs that just have "Previous"
    and "Next" buttons.

    The trick to jumping to page numbers is to precompute some number of
    pages in advance in either direction within the initial query. This
    makes it easy to generate a series of cursors based off of that data in
    order for the UI to provide the next few page numbers. This is fairly
    cheap, especially in cases like the datagrid where the query is fetching
    just PKs or other cheap columns and then following up with a full data
    fetch.

    This change implements the base support for all this. Datagrid and API
    usage will follow.

    Unit tests pass.

    Made use of this in an upcoming change for datagrids.

    Summary ID
    Add base support for keyset pagination.
    Django's database paginators work by tracking what page you're in and generating new SQL using `OFFSET` and `LIMIT` to select the right page of results to return. This has a major performance impact the further in you go, since the database often has to fetch the values first before calculating the offset into it. With heavy usage and deep lists of pages, this gets expensive fast. Keyset pagination is an alternative approach that replaces page-based offsets with "cursors". A cursor is a representation of a filter query that selects all entries before or after the current page of results. This involves selecting using sortable criteria (such as a timestamp) along with a "PK tie-breaker" (using a sortable row ID as the last criteria in case the rows otherwise match), and a query that's sorted in the same direction. With this in place, the paginator can encode a cursor that says (for example), "Select results after this last result's timestamp and PK." Or when traversing backwards, "Select results before his first result's timestamp and PK." There are some tradeoffs. You can't jump to arbitrary pages (though there's a trick here) and you can't effectively get the full result count (as that can have the same expense that pagination has for filtered results). This usually leads to UIs that just have "Previous" and "Next" buttons. The trick to jumping to page numbers is to precompute some number of pages in advance in either direction within the initial query. This makes it easy to generate a series of cursors based off of that data in order for the UI to provide the next few page numbers. This is fairly cheap, especially in cases like the datagrid where the query is fetching just PKs or other cheap columns and then following up with a full data fetch. This change implements the base support for all this. Datagrid and API usage will follow.
    4e3293ce1ccd50850ddcdadb9a6243455db7a844
    Description From Last Updated

    'typelets.json.JSONValue' imported but unused Column: 5 Error code: F401

    reviewbot reviewbot
    Checks run (1 failed, 1 succeeded)
    flake8 failed.
    JSHint passed.

    flake8