Skip to content

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:

#LocalPara quêCategoria
1resultados-queries.ts:41(handler menor) drena para mostrar olimpíadasAgregação → pode ser SQL
2resultados-queries.ts:146previewCorte.inscricoes — ids para contar classificadosAgregação → pode ser SQL
3resultados-queries.ts:237handleList.todasInscricoes — ids para join fase anteriorNecessário para filtro intra-fase, mas pode virar subselect
4resultados-queries.ts:313handleList.inscricoesfonte da paginação fakeSubstituir por range PostgREST
5resultados-queries.ts:567handleStatsAgregados.inscricoesAgregação → SQL
6resultados-queries.ts:715handleStats.inscricoesAgregação → SQL
7resultados-queries.ts:791handleBatchInit.inscricoesAgregação → SQL
8resultados-queries.ts:1017/1063helpers internosIdem 1–2
9resultados-compute.ts:148recompute — precisa ler tudo para recalcularLegí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 JS

Custo 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íada

Em SQL puro:

sql
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:

  1. Leitura UI → paginação real no servidor (range com offset/cursor + count='exact').
  2. Agregação → SQL no DB, payload de bytes.
  3. Jobs de write → drenagem (fetchAllRows) é OK, mas atrás de fila com task_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.ts em 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 fetchAllRows em 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.