02 — fetchAllRows como fonte de regressão atual
Ref: 00_INDEX · Frentes geradas: F1, F2
Contexto
fetchAllRows foi introduzido em 2026-06-04 para corrigir o Silent Data Loss do incidente Nível S (alunos do EM sumindo da lista porque PostgREST trunca em 1000 linhas sem retornar erro). O fix é correto — porém transformou um problema de dado faltando num problema de payload grande + paginação fake.
Evidência
E2.1 — Inventário de uso
8 chamadas em _shared/resultados-queries.ts, 1 em _shared/resultados-compute.ts:
| # | Local | Para quê | Categoria |
|---|---|---|---|
| 1 | resultados-queries.ts:41 | (handler menor) drena para mostrar olimpíadas | Agregação → pode ser SQL |
| 2 | resultados-queries.ts:146 | previewCorte.inscricoes — ids para contar classificados | Agregação → pode ser SQL |
| 3 | resultados-queries.ts:237 | handleList.todasInscricoes — ids para join fase anterior | Necessário para filtro intra-fase, mas pode virar subselect |
| 4 | resultados-queries.ts:313 | handleList.inscricoes — fonte da paginação fake | Substituir por range PostgREST |
| 5 | resultados-queries.ts:567 | handleStatsAgregados.inscricoes | Agregação → SQL |
| 6 | resultados-queries.ts:715 | handleStats.inscricoes | Agregação → SQL |
| 7 | resultados-queries.ts:791 | handleBatchInit.inscricoes | Agregação → SQL |
| 8 | resultados-queries.ts:1017/1063 | helpers internos | Idem 1–2 |
| 9 | resultados-compute.ts:148 | recompute — precisa ler tudo para recalcular | Legítimo (escrita em lote, ver 03) |
→ 8 de 9 usos não precisam drenar para leitura UI. Só (9) é caso legítimo (job de recompute).
E2.2 — Paginação fake (a evidência crítica)
handleListByOlimpiada em resultados-queries.ts:
linha 313: drena 100% das inscrições (fetchAllRows)
linha 408: monta `alunosComResultados` em memória
linha 470: `const isPaginated = page != null;`
linha 471+: filtra, ordena e fatia EM JSCusto por request paginada:
- DB: ≥2 round-trips (
.range(0,999)+.range(1000,1999)). - Backend: parse + JOIN aninhado de ~2k linhas.
- Rede: ~1MB JSON.
- Cliente: parse + render de 50 linhas (das 2k recebidas).
Multiplica por toda mudança de página porque a queryKey muda (useGestaoResultados.ts:252).
E2.3 — Stats que poderiam ser uma única query SQL
handleStats (linha 704) hoje drena inscricoes_olimpiada para produzir basicamente:
{ totalInscritos, classificados, medalhistas, ouro, prata, bronze } por olimpíadaEm SQL puro:
SELECT
io.olimpiada_id,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE ra.situacao = 'classificado') AS classificados,
COUNT(*) FILTER (WHERE ra.situacao LIKE 'medalha_%') AS medalhistas,
COUNT(*) FILTER (WHERE ra.situacao = 'medalha_ouro') AS ouro,
COUNT(*) FILTER (WHERE ra.situacao = 'medalha_prata') AS prata,
COUNT(*) FILTER (WHERE ra.situacao = 'medalha_bronze') AS bronze
FROM inscricoes_olimpiada io
LEFT JOIN resultados_aluno ra ON ra.inscricao_id = io.id AND ra.fase_id = $1
WHERE io.escola_id = $2 AND io.olimpiada_id = $3
GROUP BY io.olimpiada_id;→ Devolve 1 linha de ~80 bytes em vez de 2k objetos de 500 bytes. Custo no DB cresce no INDEX SCAN (linear em N), no payload é O(1).
E2.4 — A regra atual em docs/development/LARGE_DATASET_HANDLING.md
A doc atual diz: "queries que potencialmente retornam >1000 linhas SEMPRE precisam de uma estratégia de paginação. Nunca um select direto."
→ A regra está incompleta: trata "paginação" como sinônimo de fetchAllRows. Falta distinguir:
- Leitura UI → paginação real no servidor (range com offset/cursor + count='exact').
- Agregação → SQL no DB, payload de bytes.
- Jobs de write → drenagem (
fetchAllRows) é OK, mas atrás de fila comtask_id.
Essa lacuna na doc é o que permitiu o fix do incidente Nível S virar um anti-padrão de leitura UI sem perceber.
Hipóteses
P10 — Crítico. Toda leitura UI baseada em fetchAllRows é um custo escondido O(N) por request — invisível em testes (mocks de 1k linhas), letal em prod (2k+ por escola).
P11 — Conceitual. Misturamos três problemas debaixo do mesmo nome ("paginação"): leitura paginada, agregação, e drenagem para job. O SSOT precisa separar isso (proposta no 99).
P12 — Sistêmico. O mesmo padrão se replica em outras features escaláveis sempre que alguém olhar para LARGE_DATASET_HANDLING.md como ferramenta única. Sweep em 05.
O que NÃO é a causa
- NÃO é
pagination-helpers.tsem si — o helper está correto e bem testado (pagination-helpers.test.ts). O problema é onde ele é chamado. - NÃO é o cap do PostgREST. Ele continua sendo necessário e defendido por
fetchAllRowsem jobs de write.
Próximas etapas
→ Recompute (caso legítimo de drenagem, mas com problema próprio) em 03_RECOMPUTE_BATCH_HOTPATH.md → Regra revista em 99_REGRA_QUALIDADE_PERFORMANCE.md §3 ("Três caminhos para 'muitas linhas'") → Frentes F1, F2 endereçam P10–P11.