Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions backend/alembic/versions/0002_log_ingestao_execucao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Cria schema log e tabela log.ingestao_execucao.

Revision ID: 0002
Revises: 0001
Create Date: 2026-03-03
"""

from __future__ import annotations

import sqlalchemy as sa

from alembic import op

revision = "0002"
down_revision = "0001"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute("CREATE SCHEMA IF NOT EXISTS log")

op.create_table(
"ingestao_execucao",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("iniciado_em", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("status", sa.String(length=32), server_default=sa.text("'em_andamento'"), nullable=False),
sa.Column("disparado_por", sa.String(length=255), nullable=False),
sa.Column("finalizado_em", sa.DateTime(timezone=True), nullable=True),
sa.Column("counts", sa.dialects.postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column("mensagem_erro", sa.Text(), nullable=True),
schema="log",
)

op.create_index(
"ix_log_ingestao_execucao_iniciado_em",
"ingestao_execucao",
["iniciado_em"],
schema="log",
)

op.create_index(
"ix_log_ingestao_execucao_status",
"ingestao_execucao",
["status"],
schema="log",
)


def downgrade() -> None:
op.drop_index("ix_log_ingestao_execucao_status", table_name="ingestao_execucao", schema="log")
op.drop_index("ix_log_ingestao_execucao_iniciado_em", table_name="ingestao_execucao", schema="log")
op.drop_table("ingestao_execucao", schema="log")
op.execute("DROP SCHEMA IF EXISTS log")
40 changes: 40 additions & 0 deletions backend/alembic/versions/0003_sap_analytics_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Cria schema sap_analytics com as views de enriquecimento (UT e Atividade).

Revision ID: 0002
Revises: 0001
Create Date: 2025-01-02

Views criadas:
- vw_ut_enriquecida : UT + hierarquia completa (projeto/lote/bloco/subfase/fase)
- vw_atividade_enriquecida : atividade + etapa/tipo_etapa/tipo_situacao/usuario/is_finalizada
- vw_ut_atividade : join completo UT + atividade (critério 4 do sprint)
"""

from __future__ import annotations

from alembic import op
from cp.infrastructure.sap_sync.analytics_views import (
DDL_SCHEMA,
DDL_VW_ATIVIDADE_ENRIQUECIDA,
DDL_VW_UT_ATIVIDADE,
DDL_VW_UT_ENRIQUECIDA,
)

revision = "0003"
down_revision = "0002"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(DDL_SCHEMA)
op.execute(DDL_VW_UT_ENRIQUECIDA)
op.execute(DDL_VW_ATIVIDADE_ENRIQUECIDA)
op.execute(DDL_VW_UT_ATIVIDADE)


def downgrade() -> None:
op.execute("DROP VIEW IF EXISTS sap_analytics.vw_ut_atividade CASCADE;")
op.execute("DROP VIEW IF EXISTS sap_analytics.vw_atividade_enriquecida CASCADE;")
op.execute("DROP VIEW IF EXISTS sap_analytics.vw_ut_enriquecida CASCADE;")
op.execute("DROP SCHEMA IF EXISTS sap_analytics CASCADE;")
27 changes: 27 additions & 0 deletions backend/alembic/versions/0004_kpi_fato_ut_subfase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Cria tabela kpi.fato_ut_subfase para materialização de notas e estado por UT/subfase.

Revision ID: 0004
Revises: 0003
Create Date: 2025-01-03

A tabela é populada a cada sync SAP via TRUNCATE + INSERT INTO ... SELECT.
Chave primária natural: (ut_id, subfase_id).
"""

from __future__ import annotations

from alembic import op
from cp.infrastructure.sap_sync.kpi_views import DDL_TABELA_FATO_UT_SUBFASE

revision = "0004"
down_revision = "0003"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(DDL_TABELA_FATO_UT_SUBFASE)


def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS kpi.fato_ut_subfase;")
1 change: 1 addition & 0 deletions backend/db/views/sap_analytics/00_create_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE SCHEMA IF NOT EXISTS sap_analytics;
25 changes: 25 additions & 0 deletions backend/db/views/sap_analytics/vw_atividade_enriquecida.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE OR REPLACE VIEW sap_analytics.vw_atividade_enriquecida AS
SELECT
a.id AS atividade_id,
a.unidade_trabalho_id,
a.etapa_id,

e.tipo_etapa_id,
te.nome AS tipo_etapa_nome,

a.tipo_situacao_id,
ts.nome AS tipo_situacao_nome,

a.usuario_id,
a.data_inicio,
a.data_fim,
a.observacao,

(a.tipo_situacao_id = 4) AS is_finalizada
FROM sap_snapshot.macrocontrole_atividade a
JOIN sap_snapshot.macrocontrole_etapa e
ON e.id = a.etapa_id
JOIN sap_snapshot.dominio_tipo_etapa te
ON te.code = e.tipo_etapa_id
JOIN sap_snapshot.dominio_tipo_situacao ts
ON ts.code = a.tipo_situacao_id;
35 changes: 35 additions & 0 deletions backend/db/views/sap_analytics/vw_ut_enriquecida.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
CREATE OR REPLACE VIEW sap_analytics.vw_ut_enriquecida AS
SELECT
p.id AS projeto_id,
p.nome AS projeto_nome,

l.id AS lote_id,
l.nome AS lote_nome,

b.id AS bloco_id,
b.nome AS bloco_nome,

sf.id AS subfase_id,
sf.nome AS subfase_nome,

f.id AS fase_id,
tf.nome AS fase_nome,

ut.id AS ut_id,
ut.nome AS ut_nome,

ut.dificuldade,
ut.tempo_estimado_minutos
FROM sap_snapshot.macrocontrole_unidade_trabalho ut
JOIN sap_snapshot.macrocontrole_bloco b
ON b.id = ut.bloco_id
JOIN sap_snapshot.macrocontrole_lote l
ON l.id = b.lote_id
JOIN sap_snapshot.macrocontrole_projeto p
ON p.id = l.projeto_id
JOIN sap_snapshot.macrocontrole_subfase sf
ON sf.id = ut.subfase_id
JOIN sap_snapshot.macrocontrole_fase f
ON f.id = sf.fase_id
JOIN sap_snapshot.dominio_tipo_fase tf
ON tf.code = f.tipo_fase_id;
10 changes: 8 additions & 2 deletions backend/src/cp/cli/bootstrap_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine

from cp.infrastructure.sap_sync.analytics_manager import garantir_views_analytics
from cp.infrastructure.sap_sync.kpi_manager import garantir_fato_ut_subfase

_SCHEMAS_CP: tuple[str, ...] = (
"sap_snapshot",
"sap_analytics",
"auth_snapshot",
"kpi",
"agregacao",
Expand Down Expand Up @@ -73,10 +77,10 @@ def criar_banco_cp(
senha_admin: str,
nome_banco: str,
) -> bool:
"""Bootstrap do CP: garante banco e schemas iniciais (idempotente).
"""Bootstrap do CP: garante banco, schemas iniciais e views analíticas (idempotente).

Retorna True se o banco foi criado agora.
Se o banco já existir, ainda assim garante os schemas e retorna False.
Se o banco já existir, ainda assim garante os schemas/views e retorna False.
"""
criado_agora = criar_banco(
host=host,
Expand All @@ -91,5 +95,7 @@ def criar_banco_cp(
dsn_cp = f"postgresql+psycopg2://{usuario_admin}:{senha_admin}@{host}:{port}/{nome_banco}"
engine_cp = create_engine(dsn_cp, future=True)
_criar_schemas_cp(engine_cp)
garantir_views_analytics(engine_cp)
garantir_fato_ut_subfase(engine_cp)

return criado_agora
58 changes: 58 additions & 0 deletions backend/src/cp/infrastructure/sap_sync/analytics_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Gerenciador das views analíticas do schema sap_analytics.

Responsabilidade única: garantir que as views existam e estejam atualizadas.
Chamado em dois momentos do ciclo de vida:
1. Bootstrap do banco (ao subir o sistema pela primeira vez ou após deploy)
2. Após cada ingestão do pipeline SAP → sap_snapshot

Estratégia DROP + CREATE (em vez de CREATE OR REPLACE):
O PostgreSQL não permite que CREATE OR REPLACE VIEW altere a assinatura de
uma view já existente (ordem ou tipo das colunas). Para suportar evoluções
de schema sem exigir intervenção manual, fazemos DROP IF EXISTS ... CASCADE
seguido de CREATE VIEW dentro da mesma transação. O CASCADE garante que
views dependentes (ex.: vw_ut_atividade depende de vw_ut_enriquecida) sejam
derrubadas e recriadas na ordem correta.
"""

from __future__ import annotations

from sqlalchemy import text
from sqlalchemy.engine import Connection, Engine

from cp.infrastructure.sap_sync.analytics_views import (
DDL_SCHEMA,
NOMES_DAS_VIEWS,
TODAS_AS_VIEWS,
)


def garantir_views_analytics(engine_cp: Engine) -> None:
"""Cria o schema e aplica todas as views analíticas (idempotente).

Faz DROP IF EXISTS CASCADE + CREATE em transação única.
Seguro para rodar múltiplas vezes e para mudanças de assinatura.
"""
with engine_cp.begin() as conn:
_aplicar_views(conn)


def atualizar_views_analytics(conn_cp: Connection) -> None:
"""Re-aplica as views dentro de uma conexão/transação já existente.

Projetado para ser chamado no final do pipeline de sync, aproveitando
a mesma transação e evitando overhead de abertura de nova conexão.
"""
_aplicar_views(conn_cp)


def _aplicar_views(conn: Connection) -> None:
conn.execute(text(DDL_SCHEMA))
_derrubar_views(conn)
for ddl in TODAS_AS_VIEWS:
conn.execute(text(ddl))


def _derrubar_views(conn: Connection) -> None:
"""Remove todas as views em ordem inversa de dependência (CASCADE cobre o resto)."""
for nome in NOMES_DAS_VIEWS:
conn.execute(text(f"DROP VIEW IF EXISTS {nome} CASCADE"))
Loading
Loading