---
id: db-load-csv
agent: data-engineer
title: Carregar CSV com segurança (staging → merge)
inputs: [table, file, story]
outputs: [linhas carregadas idempotentemente, relatório de rejeitadas]
elicit: false
modes: [interactive, yolo]
---

# Carregar CSV com segurança (staging → merge)

**Objetivo:** carregar um CSV numa tabela alvo de forma idempotente e segura — via tabela de staging e
merge por chave — sem corromper dados existentes nem quebrar no replay.

**Pré-condições:**
- O arquivo CSV existe em `file` e a tabela alvo `table` existe. Se faltar qualquer um, **pare** e
  reporte.
- A tabela alvo tem chave natural ou única para o merge (ex.: `email`, `external_id`). Sem chave de
  conflito, o merge não tem como ser idempotente — **pare** e peça a chave.

## Passos

1. **Snapshot antes de carregar** (`*snapshot load-{table}`): carga em massa é mudança de dados;
   preciso do ponto de rollback.
2. **Inspecione o CSV:** cabeçalho, encoding, delimitador, tipos por coluna e amostra de linhas. Mapeie
   colunas do CSV → colunas da tabela. Mapeamento ambíguo eu elicito — não adivinho coluna.
3. **Crie a staging idempotente:** `CREATE TABLE IF NOT EXISTS stg_{table} (...)` com os tipos do CSV
   (texto onde houver dúvida, para validar antes de converter). `TRUNCATE stg_{table};` antes de cada
   carga.
4. **Carregue na staging** (`COPY stg_{table} FROM ...` ou loader equivalente). Sem auto-commit na
   alvo: a alvo só é tocada após validação.
5. **Valide na staging:** tipos convertem? NOT NULL respeitado? duplicatas na chave de conflito?
   linhas inválidas vão para um relatório de rejeitadas — não barram as válidas, mas são reportadas.
6. **Merge idempotente em transação:** `BEGIN; INSERT INTO {table} (...) SELECT ... FROM stg_{table}
   ON CONFLICT ({chave}) DO UPDATE/NOTHING; COMMIT;`. Rodar de novo não duplica nem corrompe.
7. **Reconcilie:** linhas no CSV vs. válidas vs. carregadas vs. rejeitadas. Os números têm que fechar.
8. **Limpe a staging** se for descartável, ou deixe-a para auditoria conforme a story pedir.

## Critério de pronto (DoD)

- [ ] Snapshot criado antes da carga
- [ ] Carga via staging + merge por chave de conflito (idempotente no replay)
- [ ] Merge rodou em transação (sem estado parcial na alvo)
- [ ] Linhas rejeitadas reportadas; contagens reconciliam (CSV = válidas + rejeitadas)
- [ ] Mapeamento de colunas rastreia à story/spec — nada inventado

## Falha / recuperação

- **Sem chave de conflito** → **pare**; merge idempotente é impossível sem ela.
- **Erro no merge** → `ROLLBACK` deixou a alvo intacta; investigo na staging e repito. Se preciso,
  `*rollback load-{table}`.
- **Muitas linhas rejeitadas** → suspendo e reporto a qualidade do CSV em vez de empurrar dado sujo.
- **Subir o resultado (push/PR)** → delego ao @devops. Eu carrego no banco, não faço push.
