minions-ai-agents/src/knowledge/standards/database_standards.md

90 lines
4.4 KiB
Markdown

# 🗄️ Padrões de Banco de Dados (O Protocolo "Integridade Relacional")
**Público:** Agentes de Backend & Arquitetos.
**Objetivo:** Construir schemas escaláveis e compatíveis que preferem PostgreSQL, mas aceitam limitações do MySQL.
> [!CRITICAL]
> **O Mandato dos Dados:**
> "Código é temporário. Dados são permanentes. Schemas quebrados são uma sentença perpétua."
## 1. 🏗️ Arquitetura & Stack
### A Camada de Abstração
- **ORM Obrigatório:** Use **SQLAlchemy (Async)** ou **Prisma** (se Node).
- **Justificativa:** Precisamos alternar entre Postgres e MySQL sem reescrever queries. SQL puro é proibido, a menos que seja para relatórios otimizados específicos.
- **Migrações:** **Alembic** (Python) ou **Prisma Migrate**.
- *Regra:* Nunca modifique o DB manualmente. Code-first sempre.
### O Duelo: PostgreSQL vs MySQL
Preferimos **PostgreSQL**.
* **Por que:** JSONB, Melhor Indexação, Confiabilidade.
* **Suporte MySQL:** Devemos suportá-lo, então evite lógica que dependa *exclusivamente* de extensões obscuras do Postgres, a menos que esteja por trás de uma feature flag.
## 2. 🏛️ Regras de Design de Schema
### Convenções de Nomenclatura (Snake_Case)
- **Tabelas:** Plural, snake_case (`users`, `order_items`, `audit_logs`).
- **Colunas:** Singular, snake_case (`created_at`, `user_id`, `is_active`).
- **Chaves:**
- Primária: `id` (UUIDv7 ou BigInt otimizado).
- Estrangeira: `target_id` (ex: `user_id` referenciando `users.id`).
### Disciplinas de Tipo
- **Timestamps:** SEMPRE use `UTC`.
- Coluna: `created_at` (TIMESTAMP WITH TIME ZONE).
- Coluna: `updated_at` (Trigger de auto-update).
- **JSON:** Use `JSONB` (Postgres) / `JSON` (MySQL).
- *Restrição:* Não trate o DB como um document store. Use JSON apenas para metadados variáveis, não para relações principais.
- **Booleans:** Use `BOOLEAN`. (MySQL define como TinyInt(1) automaticamente, o ORM lida com isso).
## 3. 🛡️ Performance & Confiabilidade
### Estratégia de Indexação
- **Chaves Estrangeiras:** DEVEM ser indexadas.
- **Busca:** Se buscar texto, use Trigram (Postgres) ou FullText (MySQL).
- **Unicidade:** Force no nível do DB (`unique=True`), não apenas no nível do código.
### O Pecado "N+1"
- **Eager Loading:** Agentes devem fazer join explícito de tabelas (`select_related` / `joinedload`).
- **Paginação:** NUNCA retorne `SELECT *` sem `LIMIT/OFFSET` (Paginação por cursor preferida para grandes conjuntos).
## 4. 🔒 Checklist de Compatibilidade (Postgres vs MySQL)
Antes de commitar uma migração, verifique:
1. **Quoting:** Postgres usa aspas duplas `"tabela"`, MySQL usa crases `` `tabela` ``. *Resultado: Use o ORM para lidar com isso.*
2. **Case Sensitivity:** MySQL no Windows é case-insensitive. Postgres é case-sensitive. *Resultado: Mantenha snake_case minúsculo explicitamente.*
3. **Enums:** ENUMs nativos são complicados em migrações. *Resultado: Use colunas VARCHAR com validação de Enum no nível da Aplicação OU tabelas de lookup.*
## 5. 🤖 A Auditoria "Auto-Query" do Agente
"Antes de eu executar esta query/migração..."
- [ ] Eu usei um arquivo de migração?
- [ ] O padrão de `created_at` está definido como `now()`?
- [ ] Estou buscando 10.000 linhas? (Adicione LIMIT).
- [ ] Se eu deletar um Pai, o que acontece com o Filho? (Defina `ON DELETE CASCADE` ou `SET NULL`).
## 6. ⏱️ Autodiagnóstico de Performance (A Checagem "Slow Query")
Agentes devem rodar estas checagens mentais ou reais em qualquer query complexa:
### Teste A: O Ritual "Explain"
Antes de finalizar uma query, simule `EXPLAIN` (Postgres) ou `EXPLAIN ANALYZE`.
* **Condição de Falha:** O resultado mostra `Seq Scan` em uma tabela com > 1000 linhas?
* **Correção:** Adicione um index na coluna filtrada (`WHERE coluna = ...`).
### Teste B: A Armadilha "Sem Limites"
* **Condição de Falha:** Uma query sem lógica de `LIMIT` ou `PAGE_SIZE`.
* **Correção:** Injete forçadamente `LIMIT 100` durante dev/teste para verificar.
### Teste C: O Detector "N+1"
* **Condição de Falha:** Usar um loop para buscar dados relacionados.
```python
users = session.query(User).all()
for user in users:
print(user.address) # 🚨 RUIM: Uma query por usuário
```
* **Correção:** Use Eager Loading.
```python
users = session.query(User).options(joinedload(User.address)).all() # ✅ BOM: Único JOIN
```