# 05 - Banco de Dados

> Conexões, convenções de tabela, multi-tenant, modelos (Sondas/Inteligências), o mini-DER do domínio central, consultas críticas e como funcionam as migrations. O comportamento do ORM em si está em [01 §6](01-arquitetura-geral.md#6-modelinteligencia-o-orm); aqui está o lado "banco".
>
> Relacionados: [12 - Armadilhas](12-armadilhas-conhecidas.md) (gotchas de ORM/ERP), [10 - Guia de Desenvolvimento](10-guia-de-desenvolvimento.md#como-criar-uma-sonda-e-uma-migration).

## 1. Topologia: PostgreSQL principal + MySQL legado

| Database | SGBD | Papel |
|---|---|---|
| `gx_goldie` / `dev_gx_goldie` | PostgreSQL | Banco principal (dados de negócio); `dev_` para desenvolvimento/testes |
| `gx_importacoes` / `dev_gx_importacoes` | PostgreSQL | Staging de importações |
| `gnesis` (alias `galaxia`) | PostgreSQL | Estrutura da plataforma (metadados de módulos, rotas, sessões) |
| `goldie_atual` | MySQL | **ERP legado** — acessado por Sondas com `db => 'mysql_goldie_atual'` |

**Roteamento de conexão** (`Galaxia\_Controladores\Connect`, singleton com pool por database):
- Default `gx_goldie`; URL contendo `dev-` reescreve automaticamente para `dev_gx_goldie`/`dev_gx_importacoes`.
- Prefixo `mysql_` no nome do banco força a instância MySQL (em dev vira `mysql_dev_goldie_atual`).
- PDO: `ERRMODE_EXCEPTION`, `FETCH_OBJ`, `SET NAMES utf8` (init command MySQL).
- Além disso, o banco de uma sessão web vem de `sectorData->inteligencia` ([01 §4.2](01-arquitetura-geral.md#42-o-objeto-galaxiauserinfo)).

Credenciais hardcoded em `system/base_config/Core/MainBoth.php` e nas classes Settings — ver [09 - Configurações §2](09-configuracoes.md#2-onde-ficam-as-credenciais) e [13 - Dívidas](13-dividas-tecnicas.md).

## 2. Convenções de tabela

### 2.1 Naming

- snake_case, plural, português: `pedidos`, `romaneios_pedidos`, `missao_logistica_itens`.
- Prefixos do núcleo da plataforma: `gx_` (`gx_empresas`, `gx_rotas`, `gx_sessoes`), `g_` (`g_users`, `g_access`), `galaxia_`.
- Prefixo de origem para dados importados: `goldie_*`, `tecnosoft_*`.
- Módulos novos por domínio: `wms_*`, `inventario_*`, `lotes*`, `edicoes_rota`, `verificacoes_otp`, `api_*`, `webhook_endpoints`.
- PK padrão: `id SERIAL PRIMARY KEY` (`BIGSERIAL` em alto volume, ex.: `api_audit_log`).
- Tipos comuns: `NUMERIC(p,s)` para quantidades/valores, `SMALLINT` para flags 0/1, `TIMESTAMP WITHOUT TIME ZONE`, `JSONB` para configs.

### 2.2 Colunas obrigatórias em toda tabela nova

**Auditoria** (injetadas pelo framework no `create()`/`update()`, protegidas contra escrita manual):

| Coluna | Semântica |
|---|---|
| `data_cadastro` / `usuario_cadastro` | criação |
| `data_atualizacao` / `usuario_atualizacao` | última edição |
| `prev_data` | snapshot anterior |
| `block_data` / `block_quem` | quando/quem soft-deletou |

**Multi-tenant** (`block_*`) — o coração do isolamento ([01 §7](01-arquitetura-geral.md#7-multi-tenant-o-mecanismo-mais-importante-do-sistema)):

| Coluna | Flag na Sonda | Semântica |
|---|---|---|
| `block` | `customerControl` | Cliente (tenant). `0` = soft-deletado; `7` = registro global |
| `block_empresa_id` | `companyControl` | Empresa |
| `block_usuario_id` | `userControl` | Usuário (sempre setada no create) |
| `block_setor_id` | `sectorControl` | Setor |
| `block_cliente_id` | — | usada nos filtros de SQL cru das Sondas V2 (ver nuance abaixo) |

⚠️ **Duas convenções de tenant coexistem:** nas queries geradas pelo framework, o tenant do cliente é a coluna `block`; em SQL cru de Sondas V2 (ex.: `Central2/PedidoLogisticaSonda`) o filtro usado é `block_cliente_id`. Migrations recentes declaram **ambos** os conjuntos. Ao escrever query manual, confirme qual coluna a tabela realmente usa.

**Regras de migration:**
- `block_*` **sem `DEFAULT`** — o framework popula; um default mascararia sessão quebrada.
- O conjunto completo (auditoria + block) espelha `wms_movimentos`; faltar coluna quebra o `create()` **só em teste de integração** ([Armadilha 7](12-armadilhas-conhecidas.md)).

### 2.3 Soft delete

Não existe `deleted_at`. `delete()` = `UPDATE ... SET block = 0`. Estado do registro pelo `block`: `0` apagado, `<customer_id>` ativo, `7` global. Todo lookup legítimo filtra `block > 0` (implícito no framework; **explícito obrigatório** em SQL cru).

### 2.4 Índices e FKs

- Padrão: índices compostos **incluindo `block`** (`(codigo, block)` unique, `(ativo, block)`), e índices por FK.
- Tabelas novas têm FK reais com `ON DELETE CASCADE` (ex.: `lotes_entidades.lote_id → lotes.id`); as antigas relacionam **por convenção, sem FK declarada** — integridade é responsabilidade da aplicação.

## 3. Modelos: onde estão e o que mapeia o quê

Regra: `Constelacoes/` = models **compartilhados** (Inteligências V1 legadas + poucas Sondas V2); `sinais/<S>/Sondas/` = models locais do Sinal (padrão V2). Ambos estendem `ModelInteligencia` — a geração é convenção, não classe base.

### 3.1 Núcleo compartilhado (Constelacoes) — principais entidades

| Domínio | Model → tabela (seleção) |
|---|---|
| **Logistica/MissaoLogistica** | `MissaoLogistica`→`missao_logistica`, `MissaoLogisticaItem`→`missao_logistica_itens`, `MissaoLogisticaUsuario`→`missao_logistica_usuarios`, `MissaoLogisticaExtrato`→`missao_logistica_extrato`, `TipoMissaoLogistica`→`tipos_missao_logistica`; Sondas V2: `PedidoErpSonda`→ERP `pedidos` (MySQL), `MissaoLogisticaItemSonda`, `ScanCotaConfigSonda` |
| **Produtos** | `Produto`→`produtos`, `ProdutoCodigoBarraSonda`→`produtos_codigos_barras` (fator de bipagem), `Estoque`→`produtos_estoque`, grupos/marcas/unidades |
| **Pedidos** | `Pedido`→`pedidos`, `Produto`→`pedidos_produtos`, ocorrências |
| **Romaneios** | `Romaneio`→`romaneios`, `RomaneioPedido`→`romaneios_pedidos`, `RomaneioVeiculo`→`romaneios_veiculos` |
| **Clientes** | `Cliente`→`clientes` + endereços, limites, horários, rankings |
| **Veiculos / Regioes / Vendedores / TabelasVenda** | `veiculos`, `entrega_regioes(_cidades)`, `vendedores` (+ gorduras, times), `tabelas_venda` |
| **Empresas** | `gx_empresas`, `gx_filiais(_empresas/_regioes)` |
| **GalaxiaEstrutural** | `g_users`, `g_access`, `gx_rotas`, `gx_cidades/estados`, `galaxia_central`, `galaxia_workflows`, `gx_linguagens` |
| **Financeiro / Metas / Servicos / Processos / Pessoas** | `contas_areceber*`, `metas*`, `servicos*`, `processos*`, `pessoas*` |

### 3.2 Sondas locais de Sinal (seleção do domínio logístico)

| Sonda | Tabela |
|---|---|
| `Edicaorota/EdicaoRotaSonda` | `edicoes_rota` |
| `AlocacaoWms/WmsMovimentoSonda` / `WmsLocalizacaoSonda` | `wms_movimentos` / `wms_localizacoes` |
| `IntegracaoWms/WmsOrdemSonda` | `wms_ordens` |
| `Lotes/Lote(Entidade/Movimentacao)Sonda` | `lotes`, `lotes_entidades`, `lotes_movimentacoes` |
| `Solicitacoes/SolicitacaoSonda(+Historico)` | `solicitacoes_globais(_historico)` |
| `Ocorrencias/OcorrenciaSonda(+Produto/+Extrato)` | `ocorrencias(_produtos/_extrato)` |
| `Inventariociclico/Inventario*Sonda` | `inventario_planos/_ciclos/_divergencias/_plano_alvos` |
| `SugestoesCodigoBarras/MissaoCodigoSugestaoSonda` | `missao_codigo_sugestao` |
| `Central/RotaCarregamento` | `rotas_carregamento` |
| `Central2/*Sonda` (dev) | `pedidos`, `romaneios(_pedidos/_veiculos)`, `missao_logistica`, `processos_status_movimentacoes` |
| `Lacres/Lacre*` | `lacres(_movimentacoes/_auditoria/_configuracao/_romaneios_veiculos)` |
| `Mensageria/Otp/Otp*Sonda` | `verificacoes_otp(_eventos)` |
| `Plataforma/Api/*Sonda` | `api_audit_log`, `api_rate_limit`, `webhook_endpoints` |
| `Dashboards/Expedicao/*Sonda` | `dashboard_expedicao_cache(_setup)` |

Para localizar a Sonda de qualquer tabela: `grep -rn "entity = ['\"]tabela" componentes/` ([11 §1](11-guia-de-manutencao.md#partindo-de-uma-tabela-do-banco)).

## 4. Mini-DER do domínio central

```mermaid
erDiagram
    clientes ||--o{ pedidos : cliente_id
    clientes ||--o{ clientes_enderecos : ""
    pedidos }o--|| entrega_regioes : regiao_id
    pedidos ||--o{ romaneios_pedidos : "pedido_id | codigoMD5"
    romaneios ||--o{ romaneios_pedidos : "romaneio_id | codigoMD5"
    romaneios ||--o{ romaneios_veiculos : romaneio_id
    veiculos ||--o{ romaneios_veiculos : veiculo_id
    pedidos ||--o{ processos_status_movimentacoes : pedido_id
    missao_logistica ||--o{ missao_logistica_itens : ""
    missao_logistica ||--o{ missao_logistica_usuarios : ""
    missao_logistica ||--o{ missao_logistica_extrato : ""
    tipos_missao_logistica ||--o{ missao_logistica : tipo_missao_id
    produtos ||--o{ missao_logistica_itens : produto_id
    produtos ||--o{ produtos_codigos_barras : ""
    produtos ||--o{ wms_movimentos : produto_id
    wms_localizacoes ||--o{ wms_movimentos : "codigo bipável"
    lotes ||--o{ lotes_entidades : "FK CASCADE"
    lotes_entidades ||--o{ lotes_movimentacoes : "FK CASCADE"
```

**Cuidados de junção (aprendidos a caro preço):**

1. `romaneios_pedidos` relaciona de **duas formas**: por id (`rp.pedido_id = pedidos.id`) **e** por hash legado (`rp.pedido = pedidos.codigoMD5`). Código antigo usa a segunda; confirme qual coluna a query vizinha usa antes de "corrigir".
2. **`pedidos.importacao_id` (Genesis) == `pedidos.codigoMD5` (ERP MySQL)** — a chave de integração. A mesma convenção `codigomd5` existe em produtos, marcas, unidades, segmentos e empresas.
3. **`numero_pedido` nunca é chave** — é número de negócio, repetível. O comentário canônico está em `Edicaorota.php`: "NUNCA usar 'pedido' como fallback: é numero_pedido (string), não pedidos.id".

## 5. Consultas críticas

### 5.1 Fila concorrente (SKIP LOCKED)

A única stored function crítica: `sp_reservar_item_da_lista` (fonte em `Constelacoes/Logistica/MissaoLogistica/Satelites/sql/`). Combina `pg_advisory_xact_lock(usuario)` (serializa o mesmo usuário) + `UPDATE ... WHERE id = (SELECT ... ORDER BY ordem_php FOR UPDATE SKIP LOCKED LIMIT 1)` (operadores em paralelo sem disputa). É chamada via `DatabaseControl->funcao()` **contornando o customerControl** — e com `p_block = 22` hardcoded na chamada (valor mágico conhecido; ver [13 - Dívidas](13-dividas-tecnicas.md)). Racional de negócio em [03 §R4](03-regras-de-negocio.md).

### 5.2 Caches (três níveis + cache materializado)

1. **RedisCache** por query quando `cacheControl => true` na Sonda.
2. **Cache "through"** (sessão/arquivo) com chaves `fdb-all-<entity>-<md5>`; invalidação por `zerarCacheDB()`.
3. **Cache de linha em disco** (`.ser` em `caching/{entity}/`), limpo automaticamente por `create/update/delete`.
4. **Tabelas materializadas**: `dashboard_expedicao_cache` (1 linha por `block, dia_efetivo, grupo_regiao_id`), populada por cron diário — o padrão "cache-first" dos dashboards ([02 §10](02-fluxos-do-sistema.md#10-fluxo-dashboard-operacional-cache-first)).

Se um dado "não atualiza", verifique os caches nesta ordem antes de suspeitar da query.

### 5.3 Riscos de SQL conhecidos

- `executarQueryFuncao()` interpola valores por string (sem bind) — só usar com valores controlados internamente, nunca input de usuário.
- Updates em lote com `id IN (implode(...))` — garantir que os ids são inteiros validados.
- Toda falha de query dispara `sendFail()` (notificação) e registro em `gc_queries_request` — barulho de erro de SQL é visível para a equipe.
- ⚠️ Sondas do `Central2` chamam `$this->query($sql, $params)`, **método que não existe** em `ModelInteligencia` (as primitivas reais são `iniciarQuery()`/`queryDinamica()`/`executarQueryFuncao()`). Como Central2 é dev-only, isso ainda não explodiu em produção — está registrado em [13 - Dívidas](13-dividas-tecnicas.md).

## 6. Migrations

**Não há runner.** Migrations são `.sql` aplicados **manualmente** (psql/PDO), e a segurança vem de convenção:

1. Arquivo `Migrations/NNN_descricao.sql` dentro do Sinal (numerado; template em `.claude/skills/genesis-sondas-db/templates/migration.sql.tpl`).
2. **Idempotente sempre**: `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS` — "rodar 2x é seguro" é requisito, porque **não existe tabela `schema_migrations`**: ninguém registra o que já foi aplicado.
3. Colunas `block_*` sem DEFAULT + conjunto completo de auditoria (§2.2).
4. Aplicar primeiro em `dev_gx_goldie`, validar com um `create()` via Sonda (teste de integração), depois em `gx_goldie`. Padrão expand/contract quando há dado de produção.
5. Os testes `MigrationSchemaTest`/`Fase2MigrationsTest` mostram o "runner" de facto: `file_get_contents` + `$pdo->exec()` ignorando erros "already exists".

Fluxo completo com exemplo em [10 - Guia de Desenvolvimento](10-guia-de-desenvolvimento.md#como-criar-uma-sonda-e-uma-migration).

## 7. Impacto de alterações (checklist antes de mexer em schema)

- **Renomear/dropar coluna:** procure usos em SQL cru (`grep -rn "nome_coluna" componentes/`) além das Sondas — Laboratórios antigos fazem JOIN manual.
- **Nova coluna NOT NULL:** precisa de default ou backfill — inserts do framework só enviam o que a Sonda conhece.
- **Tabela consumida pelo ERP/WMS:** mudanças quebram integrações silenciosamente (não há contrato formal além do código dos hooks).
- **Colunas JSONB:** o ORM devolve string — todo consumidor faz `json_decode` manual; adicionar campo dentro do JSON é barato, mudar a estrutura exige varrer os consumidores.
- **Índice novo em tabela grande de produção:** aplicar com `CREATE INDEX CONCURRENTLY` fora do arquivo idempotente padrão (não roda dentro de transação).
