Ciclo de Desenvolvimento com Banco de Dados
O ciclo de desenvolvimento do projeto é feito exclusivamente com PostgreSQL.
Durante o desenvolvimento, o PostgreSQL é tratado como o banco canônico.
Configuração do Banco de Dados (PostgreSQL)
-
Instalar PostgreSQL:
- Windows: https://www.postgresql.org/download/windows/
- Linux:
sudo apt install postgresql - macOS:
brew install postgresql
-
Configurar
.env:POSTGRES_DATABASE_URL=postgresql+psycopg://postgres:sua_senha@localhost:5432/simulador_financeiro -
Iniciar a aplicação:
pnpm backend
Na inicialização, antes de o servidor subir, o banco é criado se não existir e levado à última migration.
Ciclo de Desenvolvimento
O schema nasce dos models em backend/core/models/models.py, e cada mudança vira uma migration do Alembic, guardada em backend/migrations/versions/:
-
✏️ Alterar o model em
backend/core/models/models.py -
🧬 Gerar a migration:
pnpm db:revision "add price impact to simulations"O autogenerate compara os models com o banco do
.enve escreve a revisão. A descrição é em inglês e vira o nome do arquivo. -
🔍 Revisar o arquivo gerado — ele é um ponto de partida, não o resultado final
-
✅ Rodar
pnpm test— otests/test_migrations.pyconfere que as migrations chegam exatamente no schema dos models
O Diagrama do Banco de Dados é gerado dos models a cada pnpm start e pnpm build dos docs, sem passo manual.
O banco do .env precisa estar no head antes de gerar uma revisão nova: subir o app uma vez já garante isso.
O que o autogenerate não cobre
-
Valor novo num
ENUMnativo do Postgres (cashflow_event_type,equity_event_typeetc.) entra à mão na migration:op.execute("ALTER TYPE cashflow_event_type ADD VALUE 'TAX'") -
ENUMremovido nodowngradesobrevive aodrop_tablee é dropado explicitamente:sa.Enum(name="...").drop(op.get_bind()). -
Coluna
NOT NULLnova em tabela com dado precisa de valor para as linhas existentes:server_defaultna coluna ou umUPDATEna própria migration, antes de tornar a coluna obrigatória. -
Renomear tabela ou coluna aparece como remoção + criação; troque por
op.rename_table/op.alter_column(..., new_column_name=...)para não perder o dado. -
Regra nova da partida não pede migration. As regras ficam no JSON
simulations.rules, no formato doSimulationSettingsDTO: a regra nova é um campo do DTO com valor padrão, e as partidas salvas abrem com esse padrão. Regra removida pede uma migrationrules - 'chave', porque a leitura recusa chave desconhecida.
Uma migration por versão
No release de cada versão, as migrations que nasceram nela se juntam numa só, que leva o ID da última delas para os bancos de desenvolvimento que já estavam no fim se reconhecerem. A linha de revisões começa na migration inicial, que é o banco da última versão que o jogador ainda pode ter. Juntar não quebra quem atualiza de fora, mas cortar o caminho de uma versão antiga é perda para quem atualiza e só acontece numa versão maior (versionamento).
Antes de apagar as migrations antigas, confira que o pg_dump --schema-only de um banco feito pela cadeia antiga é igual ao de um banco feito pela nova, inclusive a ordem das colunas e dos valores dos enums.
Como a Migration Roda na Inicialização
backend/core/migration.py decide pelo estado do banco:
| Estado do banco | O que acontece |
|---|---|
| Vazio | Recebe todas as migrations até o head |
| Já no head | Nada |
Com tabelas, sem alembic_version, igual aos models | É marcado no head (stamp) |
Com tabelas, sem alembic_version, diferente dos models | A inicialização é recusada: o banco é anterior às migrations e precisa ser trocado por um novo |
| Numa revisão anterior | Backup, e então upgrade |
| Numa revisão que esta versão não conhece | Recusado: o banco é de uma versão anterior à que a linha de revisões cobre; abra-o uma vez com a versão anterior e volte |
Backup: antes do upgrade, o banco como estava é copiado para <banco>_bkp_AAAAMMDD_HHMMSS; ficam os 3 mais recentes. Remover ou renomear tabela e coluna é permitido: o backup é o caminho de volta.
O backup usa CREATE DATABASE ... TEMPLATE, que exige o banco sem outras conexões abertas: com pgAdmin ou DBeaver conectados, a migration é recusada até que eles sejam fechados. O DDL do Postgres é transacional, então uma migration que falha no meio é desfeita inteira.
Dicas e Boas Práticas
Evite SQL Raw
Sempre que possível, use o ORM do SQLAlchemy.
Evite:
session.execute("SELECT * FROM users WHERE id = 1")
Prefira:
session.get(User, 1)
ou, em consultas mais complexas:
stmt = select(User).where(User.id == 1)
session.execute(stmt).scalar_one_or_none()