Pular para o conteúdo

Paginação por cursor

Por que pagina=3 mente, e o que o cursor promete no lugar.

Paginar por número (?pagina=3&por=20) é a forma mais comum, e ela pula e repete linhas em qualquer lista que muda: alguém insere na página 1, e o item que era o 20 vira o 21 — quem pede a página 2 nunca o vê.

Por númeroPor cursor
"me dê a página 3"OFFSET 40 — o banco varre 40 linhas e joga foraWHERE id > 'abc' — usa o índice
alguém insere no meiolinha pulada ou repetidaestável: o cursor aponta para uma linha
"ir para a página 50"funcionanão existe, e é o preço
custo em 1 milhão de linhascresce com o deslocamentoconstante
dataforge
adopt Arcane.Lavra as Lavra

record Produto:
    id: Integer
    nome: String

TODOS := [Produto(i, $"produto {i}") cycle i in range(1, 31)]

action listar(raiz, args, ctx):
    // 'pagina' fatia sozinha: ela recebe a coleção INTEIRA, quantos
    // itens e o cursor — e devolve o recorte com as bordas e a info.
    // E o '??' não é opcional: uma chave que não veio não está no
    // vault, e indexá-la levanta.
    yield Lavra.pagina(TODOS, args["primeiros"] ?? 10,
                       args["depois"] ?? void, len(TODOS))

esq := Lavra.esquema("catalogo")
Lavra.tipo(esq, Produto)
Lavra.tipo_pagina(esq, "Produto")
Lavra.busca(esq, "produtos", "PaginaProduto!",
    args := {"primeiros": {"tipo": "Integer", "padrao": 10},
             "depois": "String"},
    resolve := listar)
Lavra.conferir(esq)

r := Lavra.executar(esq, """
busca:
    produtos(primeiros: 3):
        total
        info:
            tem_proxima
            cursor_fim
        itens:
            id
            nome
""")
pagina := r["dados"]["produtos"]
assert len(pagina["itens"]) is 3
assert pagina["total"] is 30
assert pagina["info"]["tem_proxima"] is yes
out pagina

A segunda página#

dataforge
adopt Arcane.Lavra as Lavra

record Produto:
    id: Integer

TODOS := [Produto(i) cycle i in range(1, 11)]

action listar(raiz, args, ctx):
    yield Lavra.pagina(TODOS, args["primeiros"] ?? 3,
                       args["depois"] ?? void, len(TODOS))

esq := Lavra.esquema("c")
Lavra.tipo(esq, Produto)
Lavra.tipo_pagina(esq, "Produto")
Lavra.busca(esq, "produtos", "PaginaProduto!",
    args := {"primeiros": "Integer", "depois": "String"}, resolve := listar)
Lavra.conferir(esq)

primeira := Lavra.executar(esq,
    "busca:\n    produtos(primeiros: 3):\n        info:\n            cursor_fim\n        itens:\n            id")
cursor := primeira["dados"]["produtos"]["info"]["cursor_fim"]

segunda := Lavra.executar(esq,
    $"busca:\n    produtos(primeiros: 3, depois: \"{cursor}\"):\n        itens:\n            id")
assert segunda["dados"]["produtos"]["itens"][0]["id"] is 4
out $"a segunda página começa no {segunda['dados']['produtos']['itens'][0]['id']}"

O cursor é opaco, de propósito#

Um cursor que é visivelmente o id convida o cliente a construí-lo, e aí a implementação não pode mais mudar: trocar de offset para keyset quebraria todo mundo. Codificá-lo (base64, ou assinado) é o que mantém a liberdade — e é o mesmo motivo de Kiln.cursor existir do lado REST.