Banco — ingestão e observação¶
10 tabelas de 63. A ferramenta conectada, a credencial, a execução da coleta, o checkpoint,
o cru preservado, e o repositório nas suas três camadas. Recorte declarado em
mapa-das-tabelas.md.
O diagrama¶
erDiagram
tenants ||--o{ connected_tools : delimita
connected_tools ||--o{ tool_credentials : "autentica com"
connected_tools ||--o{ tool_observation_events : "encerra e retoma"
connected_tools ||--o{ syncs : executa
tool_credentials |o--o{ syncs : "foi usada em"
syncs ||--o{ sync_checkpoints : "retoma por"
syncs ||--o{ raw_payloads : preserva
connected_tools ||--o{ observed_repositories : alcança
sys_swo_loaded_software_system_copies ||--o{ cmpo_source_repositories : materializa
cmpo_source_repositories ||--o{ observed_repositories : "é observado como"
eo_organizations ||--o{ cmpo_source_repositories : possui
observed_repositories ||--o{ cmpo_branches : tem
connected_tools {
uuid id PK
uuid tenant_id FK
string tool_type
string instance_url
string organization_login
int sync_interval_minutes "nulo = sem coleta automática"
datetime last_sync_at
datetime needs_attention_since "nulo = saudável"
string needs_attention_reason
}
tool_credentials {
uuid id PK
uuid tenant_id FK
uuid connected_tool_id FK
string label
bytea secret "cifrado pelo Cloak"
string last_four
bool active
string owner_login
text_array scopes
datetime validated_at "nulo = nunca validada"
datetime last_failure_at
string last_failure_reason
}
tool_observation_events {
uuid id PK
uuid tenant_id FK
uuid connected_tool_id FK
string event "ended ou resumed"
datetime occurred_at
uuid actor_user_id FK
string reason
json impact
}
syncs {
uuid id PK
uuid tenant_id FK
uuid connected_tool_id FK
uuid credential_id FK
string status "running, completed, failed ou interrupted"
datetime started_at
datetime finished_at "nulo = em curso"
int records_collected
int records_created
int records_updated
int records_skipped
int repositories_unreachable
int repositories_skipped
json skip_reasons
int memberships_pending_role
string error_reason
uuid interrupted_by_user_id FK
}
sync_checkpoints {
uuid id PK
uuid tenant_id FK
uuid sync_id FK
string entity_type
string cursor
int page_count
int record_count
int expected_count
datetime last_page_at
string status
}
raw_payloads {
uuid id PK
uuid tenant_id FK
uuid sync_id FK
string raw_entity_type
string external_id
json payload
string mapping_id
int mapping_version
datetime collected_at
}
sys_swo_loaded_software_system_copies {
uuid id PK
uuid tenant_id FK
string internal_id
string type "CHECK = source_repository"
string external_id
datetime no_longer_observed_at
}
cmpo_source_repositories {
uuid id PK
uuid tenant_id FK
uuid loaded_software_system_copy_id FK
uuid organization_id FK
string name
string qualified_name
string url
string primary_language
string default_branch
datetime archived_at
datetime last_pushed_at
}
observed_repositories {
uuid id PK
uuid tenant_id FK
uuid connected_tool_id FK
uuid source_repository_id FK
datetime excluded_at "decisão do tenant"
uuid excluded_by_user_id FK
datetime inaccessible_since "falha de alcance"
string inaccessible_reason
datetime issues_collected_at
datetime comments_collected_at
datetime changes_collected_at
datetime verifications_collected_at
datetime branches_collected_at
datetime reviews_collected_at "coluna morta"
int branches_total
json query_versions
}
cmpo_branches {
uuid id PK
uuid tenant_id FK
uuid observed_repository_id FK
string name
string head_sha
datetime head_committed_at
bool is_default
bool is_protected
datetime no_longer_observed_at
}
Os índices parciais¶
| Índice | Forma | Afirma |
|---|---|---|
syncs_one_running_per_tool_index |
UNIQUE (connected_tool_id) WHERE status = 'running' |
uma coleta em curso por ferramenta — FR-018. O unique do worker Oban sozinho não bastaria: ele expira em 300 s, e uma coleta longa passa disso |
connected_tools_auto_sync_index |
INDEX (sync_interval_minutes) WHERE sync_interval_minutes IS NOT NULL |
não é invariante — é a pergunta que Jobs.ScheduleDueSyncs faz a cada 5 min, servida sem varredura |
Os CHECK¶
| Constraint | Regra |
|---|---|
syncs_status_check |
running, completed, failed ou interrupted — quatro, e nada mais |
observed_repositories_exclusion_has_author |
excluded_at nulo, ou excluded_by_user_id presente |
tool_observation_events_event_check |
ended ou resumed |
sys_swo_copies_type_check |
type = 'source_repository' — a cópia carregada é sempre repositório, hoje |
Não há CHECK que exija autor para inaccessible_since, e é coerente: exclusão é
decisão de pessoa e por isso tem autor; inacessibilidade é fato observado pela
credencial e não tem autor humano. As duas impedem a coleta, e o modelo as distingue.
As FKs e o que acontece ao apagar¶
| De | Para | Ao apagar |
|---|---|---|
tool_credentials.connected_tool_id |
connected_tools |
CASCADE |
syncs.connected_tool_id |
connected_tools |
CASCADE |
syncs.credential_id |
tool_credentials |
SET NULL — a execução fica no histórico sem a credencial |
syncs.interrupted_by_user_id |
users |
SET NULL |
sync_checkpoints.sync_id |
syncs |
CASCADE |
raw_payloads.sync_id |
syncs |
CASCADE |
observed_repositories.connected_tool_id |
connected_tools |
CASCADE |
observed_repositories.source_repository_id |
cmpo_source_repositories |
CASCADE |
observed_repositories.excluded_by_user_id |
users |
SET NULL |
cmpo_source_repositories.loaded_software_system_copy_id |
sys_swo_...copies |
CASCADE |
cmpo_source_repositories.organization_id |
eo_organizations |
RESTRICT |
cmpo_branches.observed_repository_id |
observed_repositories |
CASCADE |
cmpo_branches.tenant_id |
tenants |
CASCADE — exceção, ver abaixo |
demais .tenant_id |
tenants |
RESTRICT |
cmpo_branches.tenant_id é CASCADE enquanto as outras nove são RESTRICT. Não encontrei
no código a razão escrita da diferença. Duas leituras: ou foi decisão (branch é derivado do
repositório e some junto), ou é inconsistência de uma migração posterior. Levar a quem mantém
a ingestão — não é escolha deste documento.
A cascata mais longa do banco¶
tenants × connected_tools → observed_repositories → cmpo_branches
→ syncs → sync_checkpoints
→ raw_payloads
Apagar um connected_tools leva junto credenciais, eventos, execuções, checkpoints, payloads
crus, repositórios observados e branches. Apagar um tenants não leva nada — é recusado
pelo RESTRICT na primeira FK. A assimetria é a regra: a ferramenta é operacional e
descartável; o tenant é raiz e só sai depois de tudo o que ele delimita.
O que o diagrama não mostra¶
inserted_at/updated_at;tool_observation_eventsusatimestamps(updated_at: false).source_system,source_instance,external_id,collected_at,last_observed_atnas tabelas coletadas — a Application Reference (FR-012).cmpo_source_repositoriesesys_swo_...copiesos têm;connected_tools,syncsesync_checkpointsnão, porque são plataforma e não dado observado.record_versioneinternal_idemsys_swo_...copies.cmpo_branches.raw_payloadecmpo_branches.external_created_at.- Os índices não parciais — há dezenas, e nenhum carrega invariante.
Ingestion.Cota,Ingestion.JanelaeIngestion.QueryVersionnão têm tabela: a cota vive em processo (ADR 0007), a janela é função, e a versão de consulta grava emobserved_repositories.query_versions.
O que o dado diz hoje¶
Banco de desenvolvimento, 2026-09-12:
| Fato | Valor |
|---|---|
| ferramentas conectadas / credenciais | 1 / 1 |
| execuções de coleta | 9 |
| checkpoints | 103 |
| payloads crus preservados | 16 820 |
| repositórios, nas três camadas | 126 / 126 / 126 — sem divergência |
| branches | 755 |
eventos de observação (ended/resumed) |
0 |
As três camadas do repositório terem exatamente 126 linhas cada é o que se espera quando a coleta está consistente; um dia em que divergirem, a diferença é o achado, e este número é a linha de base para percebê-lo.
Divergência¶
observed_repositories.reviews_collected_at está no ERD com a nota coluna morta, e não
no schema Ecto. Detalhe e encaminhamento em
classes/ingestao-e-observacao.md.