LIMIT 20 OFFSET 4000 is how most APIs paginate, and it has two well-known problems: the database must walk and discard 4000 rows to serve page 200, and if a row is inserted or deleted while a user is paging, they silently see a duplicate or miss a record entirely.
Cursor (or keyset) pagination fixes both. Instead of "skip N rows", the client sends back an opaque marker meaning "resume after this specific row".
Implement paginate(rows, cursor, limit):
rows — the full dataset, a list of {"id": int, "name": str} sorted by id ascendingcursor — an opaque string from a previous call, or None for the first pagelimit — maximum items to returnReturn {"items": [...], "next_cursor": <str | None>}, where:
items holds up to limit rows whose id is strictly greater than the cursor's idnext_cursor encodes the id of the last item returned, or is None when no rows remain after this pageThe cursor is the base64 of the id, so that clients treat it as opaque rather than doing arithmetic on it:
base64.urlsafe_b64encode(str(row_id).encode()).decode() # 20 -> "MjA="
Example (ids 10, 20, 30):
paginate(rows, None, 2)→{"items": [10, 20], "next_cursor": "MjA="}paginate(rows, "MjA=", 2)→{"items": [30], "next_cursor": None}
Constraints:
limit >= 1; ids are unique positive integers, ascending, not necessarily contiguous{"items": [], "next_cursor": None}next_cursor must be None on the last page — never a cursor pointing at nothingOfficial solution locked
Give the problem a real attempt before peeking — revealing it affects your credit for this problem for 4 hours.