# Auditoria de arquitetura, performance e qualidade

Data: 21/07/2026
Escopo: leitura estática do código Python, migrations, templates SQL, scripts e
documentação. Não houve carga sintética nem `EXPLAIN` contra dados reais.

## Status de implementacao

Concluido em 21/07/2026:

- P0: conexao persistente, insert multi-row, lotes transacionais e fila limitada.
- P0: isolamento fisico por SIAFI, ambiente e perfil.
- P0 operacional: runner de migrations com lock, checksum e retry de deploy.
- P1: um unico certificado ativo por municipio, garantido no MySQL 5.6.
- P1: transacoes nas gravacoes relacionadas de municipio, certificado, fila, estado e log.
- P1: IBGE opcional e apenas informativo; SIAFI e a identidade operacional canonica.
- P1: exemplos, seed, painel, workers e documentacao operacional alinhados a SIAFI.
- Testes: commit/rollback unitarios e cenarios de conflito INSERT/UPDATE no MySQL 5.6.

## Resumo executivo

O projeto tem uma boa direção: tabelas volumosas separadas do catálogo, campos
normalizados além do XML e índices iniciais para data, prestador e tomador. Porém
ele ainda não está preparado para milhões de documentos por tenant. A prioridade
não é criar muitos outros índices: é corrigir o caminho de escrita, fechar a
chave de isolamento e só então medir as consultas reais.

| Prioridade | Achado | Risco |
| --- | --- | --- |
| P0 | `MysqlCli` abre o binário `mysql` para cada `execute`/`query_rows`. | Milhões de processos/conexões e round-trips; sincronização inviável em volume. |
| P0 | Ambiente/perfil fazem parte do catálogo, mas a tabela física é derivada somente de SIAFI e é única. | Colisão de escopo ou impossibilidade de cadastrar a mesma entidade em mais de um ambiente/perfil. |
| P0 | O lock da sincronização é verificado e atualizado em comandos separados e o painel cria uma thread por disparo. | Duas sincronizações podem concorrer; não há limite global de workers. |
| P1 | Documentação e exemplos divergem entre identificador IBGE e SIAFI. | Criação/consulta de tabelas incompatíveis e manutenção perigosa. |
| P1 | Não há transação de aplicação para operações multi-tabela nem garantia de um único certificado ativo. | Estado parcial, município duplicado na listagem e certificado ativo ambíguo. |
| P1 | Não há testes automatizados de unidade/integração nem medição de plano. | Regressão silenciosa de regras e performance. |
| P2 | `app.py` concentra roteamento, HTML, métricas e parsing; `sync.py` concentra rede, parse e persistência. | DRY e manutenção pioram ao adicionar funcionalidades. |

## 1. Bloqueadores de escala

### 1.1 Cliente MySQL por operação — P0

`src/nf_nacional_web/db.py` usa `subprocess.run` no método `execute`. Durante a
sincronização, cada documento pode executar `INSERT` do documento + `SELECT
LAST_INSERT_ID` + `INSERT` das partes, além de atualizações de progresso. Com um
milhão de itens isso se torna milhões de processos e conexões MySQL.

**Correção recomendada antes da carga:** substituir esse módulo por um driver
MySQL com conexão persistente e parâmetros (`mysqlclient`, `PyMySQL` ou
equivalente compatível com Python 3.12/MySQL 5.6). Processar documentos em lotes
e confirmar uma transação por lote. Manter o cliente CLI apenas para scripts
operacionais, se necessário.

### 1.2 Isolamento físico incompleto — P0

`nfse_municipios` permite combinações de `codigo_ibge`, `ambiente` e `perfil`,
mas os nomes das tabelas são produzidos somente com `codigo_siafi`. Além disso,
há unicidade individual em `tabela_documentos`, `tabela_eventos` e
`tabela_partes`. Na prática uma segunda combinação ambiente/perfil não pode
receber tabelas próprias. Eventos também são únicos por chave/tipo/sequência,
sem `ambiente`.

**Decisão obrigatória:**

- se cada ambiente/perfil deve ficar isolado, incluí-los na chave e no sufixo da
  tabela física, por exemplo `{siafi}_{ambiente}_{perfil}`; ou
- se devem compartilhar a tabela, remover a falsa expectativa de isolamento do
  catálogo e incluir `ambiente` em todas as chaves naturais e índices de leitura.

Não misturar os dois modelos. A migração deve ocorrer antes da primeira carga
real, pois renomear e reindexar tabelas com milhões de linhas será caro.

### 1.3 Lock e concorrência — P0

`trigger_sync` lê o estado e depois o atualiza. Entre essas duas operações outro
request pode fazer o mesmo. Em seguida cada request cria uma `thread` daemon,
sem semáforo global. O worker em lote também deve receber limite explícito e
observabilidade de fila.

**Correção recomendada:** trocar por uma única atualização condicional (lock
atômico) e verificar `rowcount`; usar executor/fila com `NFSE_SYNC_MAX_WORKERS`;
garantir liberação do lock em `finally` e testes de corrida.

## 2. Índices e padrão de consultas

O template atual já cobre os filtros básicos de documento por data, prestador,
tomador, status e competência. Isso é uma base adequada para começar, mas não
substitui o desenho das telas/API que ainda não existem.

| Caminho de leitura | Índice a validar | Consulta segura |
| --- | --- | --- |
| Documentos por período | `(data_emissao, id)` | período obrigatório, cursor `data_emissao,id` e `LIMIT`. |
| Prestador por período | `(prestador_documento, data_emissao, id)` | igualdade do documento + intervalo de datas. |
| Tomador por período | `(tomador_documento, data_emissao, id)` | igualdade do documento + intervalo de datas. |
| Status por período | `(status_documento, data_emissao, id)` | somente se esta for uma tela real. |
| Eventos do documento | `(documento_id)` | já existe no template. |
| Retomada por NSU | `(ambiente, nsu)` | já existe como chave única; consultar com `WHERE ambiente = ?`. |

Os índices existentes sem `id` ainda podem atender leituras pequenas. Acrescentar
`id` só deve ser decidido com `EXPLAIN` da paginação adotada; índices redundantes
tornam a ingestão mais lenta. Para o atual `_max_stored_nsu`, mudar a consulta
para filtrar `ambiente`, aproveitando a chave única `(ambiente, nsu)`, em vez de
calcular `MAX(nsu)` sobre todos os ambientes.

Nunca use `OFFSET` em páginas profundas ou `DATE(data_emissao) = ...`. O formato
obrigatório é:

```sql
SELECT id, nsu, chave_acesso, data_emissao, prestador_documento, tomador_documento, status_documento
FROM nfse_documentos_<tenant>
WHERE data_emissao >= ? AND data_emissao < ?
  AND (data_emissao < ? OR (data_emissao = ? AND id < ?))
ORDER BY data_emissao DESC, id DESC
LIMIT ?;
```

Caso ambiente compartilhe a tabela, ele deve ser o primeiro predicado e o
primeiro componente dos índices correspondentes.

## 3. DRY e boas práticas

### Pontos positivos

- Extração de XML, criptografia, tenants e sincronização já estão em módulos
  distintos.
- Existe template central para as tabelas de volume e versionamento por tenant.
- Há validação de códigos de município antes de gerar nome de tabela.
- XML bruto é separado de campos normalizados, permitindo listagens leves.

### Pontos a corrigir

1. **SQL por interpolação:** há SQL formatado em múltiplos módulos. Hoje os
   códigos são majoritariamente validados, mas a regra é frágil e repetida.
   Um repositório com parâmetros elimina risco de escaping e reduz duplicação.
2. **Operações repetidas de certificado/log:** desativar certificado, inserir
   certificado e registrar log aparecem em mais de um fluxo. Extrair helpers
   transacionais e impor a regra de no máximo um certificado ativo.
3. **Mistura de camadas:** separar templates/renderização e handlers de `app.py`;
   separar cliente ADN, parser e repositório de `sync.py` ao implementar o driver
   persistente.
4. **Configuração dispersa:** nomes, timeout, URL ADN, limites de lote e
   concorrência devem viver em uma configuração validada na inicialização.
5. **Testes:** criar testes de parser XML, validação, geração segura dos nomes,
   idempotência de upsert, lock concorrente e integração com MySQL local.

## 4. Roteiro de correção

1. Definir oficialmente a chave de tenant (SIAFI versus IBGE) e o isolamento de
   ambiente/perfil; corrigir código, migrations, mock e documentação em conjunto.
2. Trocar o acesso por CLI por driver persistente, parâmetros, transação e
   inserção em lote. Medir documentos/segundo e tempo de retomada.
3. Implementar lock atômico e fila limitada; cobrir corrida com teste.
4. Criar as consultas de leitura com período, keyset e projeção leve; para cada
   uma, adicionar/ajustar apenas o índice confirmado por `EXPLAIN` em dados de
   produção anonimizados ou carga representativa.
5. Adicionar suite de testes e pipeline que rode compilação, smoke e migrations
   em banco limpo.

## 5. Critério para liberar carga real

- chave de tenant e escopo de ambiente/perfil sem ambiguidade;
- zero sincronizações concorrentes para o mesmo município;
- driver persistente e commits em lote;
- recuperação idempotente após interrupção;
- `EXPLAIN` sem full scan nas listagens previstas;
- teste de carga com volume e distribuição próximos do município mais intenso;
- backup, retenção de XML/log e procedimento de rollback documentados.
