Manuseio de Grandes Volumes de Dados (SSOT)
Status: Padrão obrigatório a partir de 2026-06-04 Aplicabilidade: TODA Edge Function que consulta tabelas que podem crescer com volume escolar (alunos, inscrições, resultados, mensagens, logs, etc.).
1. Contexto / por que existe
O PostgREST (camada HTTP do Supabase) aplica um cap global de linhas por resposta — db-max-rows, default 1000. Quando uma query retorna mais que esse cap, a resposta não vem com erro: ela vem truncada silenciosamente para 1000 linhas, sem cabeçalho de aviso visível ao cliente JS.
Para uma escola com >1000 inscrições em uma olimpíada, isso significa que:
- A lista de alunos da tela "Resultados" perde linhas (alunos do "fim" da ordenação somem — historicamente os níveis do ensino médio).
- Filtros derivados em memória (
niveis,series,turmas) ficam incompletos (níveis "S" e "J" simplesmente não aparecem nos dropdowns). - Stats agregados desbalanceiam (cards "Participantes", "Classificados" ficam errados para baixo, sem nenhum alerta).
- A coordenação não publica resultados porque "não tem dado lá".
Este é um padrão de falha silenciosa categorizado como Silent Data Loss (ver docs/security/SILENT_DATA_LOSS_AUDIT.md). O incidente 2026-06-04 (Nível S / J Canguru) foi causado exatamente por isso.
2. Regra de ouro
Queries que potencialmente retornam >1000 linhas SEMPRE precisam de uma estratégia de paginação. Nunca um
selectdireto.
Duas estratégias canônicas, ambas no _shared/:
| Estratégia | Quando usar | Implementação |
|---|---|---|
queryInChunks(...) | Você tem uma lista de IDs e faz IN(...) | _shared/resultados-compute.ts — particiona o array em chunks de 80 |
fetchAllRows(buildQuery) | Você filtra por .eq()/.in() em um escopo amplo (escola, ano) e não tem uma lista de IDs | _shared/pagination-helpers.ts — drena via .range(from, to) |
import { fetchAllRows } from "../_shared/pagination-helpers.ts";
const inscricoes = await fetchAllRows<{ id: string }>(
() => supabase.from("inscricoes_olimpiada")
.select("id, aluno_id, nivel_id")
.eq("escola_id", escolaId)
.eq("olimpiada_id", olimpiadaId),
{ label: "inserir-resultados.inscricoes" },
);Importante: o argumento é uma factory — Supabase QueryBuilders são single-shot quando awaitados; cada iteração precisa de uma nova instância.
3. Checklist obrigatório de revisão (PR)
Quando o PR adiciona ou altera uma query, marcar mentalmente:
- [ ] A tabela cresce proporcional ao tamanho de uma escola? (alunos, inscrições, resultados, contatos, mensagens, logs)
- [ ] A query pode retornar >1000 linhas para a maior escola da base?
- [ ] Se sim, usa
fetchAllRows(sem.in(ids)) ouqueryInChunks(com.in(ids))? - [ ] Frontend: a feature usa React Query com paginação no servidor (page/pageSize) e debouncing de busca? Nunca slice no cliente sobre uma lista capada.
- [ ] Stats/contadores calculados em backend são feitos sobre a coleção drenada (não sobre a página atual)?
- [ ] Existe teste Deno cobrindo o cenário ">1000 linhas"? Mockar
rangee validar que o helper drenou todas as páginas.
4. UI — paginação e debounce
A camada de leitura paginada já existe em useResultadosPorOlimpiadaPaginado (50 linhas por página, sort/filter no servidor, search com debounce). É o padrão de referência:
staleTime: dado operacional → 30s–5minkeepPreviousData: evita flash de empty statequeryKeyincluiescola_id, olimpiada_id, fase_id, nivel_id, page, busca- Busca → debounce ≥300ms no input antes de invalidar query
- Backend ordena (
order().range()) — nunca confiar em ordem natural
Para listas pequenas (≤100 linhas) é aceitável renderizar tudo, mas ainda sem depender do cap implícito do PostgREST.
5. Import (zero-trust)
Já documentado em docs/development/IMPORT_SYSTEM_STANDARD.md:
- Batches de 50–100 linhas no upsert
- Validação server-side mesmo quando o frontend já validou
EdgeRuntime.waitUntilpara trabalho >25s sem segurar a Response
Nada muda aqui — este documento complementa o pipeline de leitura.
6. Auditoria histórica
| Incidente | Causa | Fix |
|---|---|---|
| 2026-06-04 — Nível S/J Canguru sumidos da tela Resultados de uma escola com 1.4k inscrições | Cap silencioso de 1000 do PostgREST em 8 queries de inscricoes_olimpiada (handleList, handleStats, handleBatchInit, handleStatsAgregados, internals, previewCorte, fetchOlimpiadasList, fase-anterior) | Helper fetchAllRows + refactor em _shared/resultados-queries.ts. Snapshot de publicação já usa paginação (snapshot-publicados.ts) — não houve perda de dado em banco. |
7. Referências
supabase/functions/_shared/pagination-helpers.ts— helper + testessupabase/functions/_shared/resultados-compute.ts—queryInChunks(IN-based)supabase/functions/_shared/snapshot-publicados.ts— exemplo de pagina range já consolidadodocs/audits/AUDIT_RESULTADOS_NIVEL_S_2026-06-04.md— incidentedocs/security/SILENT_DATA_LOSS_AUDIT.md— taxonomia da falhamem://architecture/large-dataset-handling-standard— memória de regra