"""Keyset paging for newest-first lists that load as you scroll (#5211, pref 172). A page is the rows strictly older than a cursor, ordered (time, id) newest first, so a row added while someone scrolls never shifts the pages after it, and no page needs an OFFSET. The cursor is "|": opaque to the browser, which only hands back the one the last page gave it. stmt = keyset(select(AuditEvent), AuditEvent.timestamp, AuditEvent.id, request.args.get("before")) rows = (await db.execute(stmt)).scalars().all() rows, after = next_page(rows, lambda e: (e.timestamp, e.id)) Templates end each page with scroll_more() while `after` is set, and scroll_end() once it is not (_macros.html). """ from __future__ import annotations from datetime import datetime from typing import Callable from sqlalchemy import tuple_ # Rows per page: enough to fill a tall screen past the scroll margin, so the # next page starts loading before the reader reaches the end of this one. PAGE_SIZE = 100 def cursor(at: datetime, row_id) -> str: return f"{at.isoformat()}|{row_id}" def parse_cursor(raw: str | None) -> tuple[datetime, str] | None: """(time, id) from a cursor, or None for no cursor or one that does not parse (a mangled cursor starts the list over rather than failing the page).""" if not raw or "|" not in raw: return None at, row_id = raw.split("|", 1) try: return datetime.fromisoformat(at), row_id except ValueError: return None def keyset(stmt, at_col, id_col, before: str | None, size: int = PAGE_SIZE): """`stmt` limited to one page older than `before`, newest first. Fetches one row more than a page so next_page can tell whether another follows.""" parsed = parse_cursor(before) if parsed is not None: stmt = stmt.where(tuple_(at_col, id_col) < tuple_(*parsed)) return stmt.order_by(at_col.desc(), id_col.desc()).limit(size + 1) def next_page(rows, key: Callable, size: int = PAGE_SIZE) -> tuple[list, str | None]: """(this page's rows, the cursor for the next page or None at the end). `key(row)` is the row's (time, id).""" rows = list(rows) if len(rows) <= size: return rows, None rows = rows[:size] return rows, cursor(*key(rows[-1]))