Lineage em Nível de Coluna¶
O Provisa rastreia lineage de dados em nível de coluna de forma estática — computado a partir de definições SQL e contratos de command, sem execução necessária. Duas views estão disponíveis: um DAG por declaração e um grafo de proveniência federation-wide abrangendo todas as views e views materializadas (MVs) registradas.
O explorador de lineage¶
Navegue até Lineage na UI (/lineage). Cole uma declaração SQL e clique em Build statement
graph para ver seu DAG em nível de coluna. Clique em Federation graph para carregar o grafo de
proveniência sobre todas as MVs no registro. [tool-verified: LineagePage.tsx:28-119]
DAG em nível de declaração (REQ-1160)¶
Cada coluna de saída nomeada no seu SQL se torna um nó. O builder a rastreia de volta através de todo CTE, subconsulta, join, e chamada de command inline até suas colunas de fonte, construindo um grafo direcionado das entradas de fonte até as saídas finais.
Exemplo trabalhado¶
SELECT o.id, e.embedding, upper(e.geo) AS geo_u
FROM orders o
JOIN enrich_grpc_set('main.public.orders') e ON o.id = e.id
Esta declaração produz três colunas de saída. O grafo para geo_u se parece com:
orders.geo ──[enrich_grpc_set(...)]──► e.geo ──[UPPER]──► geo_u
orders.id ─╮ (taint closure)
orders.region ─╯
orders.id,orders.region, eorders.geosão nós de fonte (o contrato de entrada estreito deenrich_grpc_setdeclaraideregion; o fechamento de contaminação completo conecta todas as entradas declaradas a todas as saídas). [tool-verified:_splice_commandsin graph.py:223-242]e.embeddingee.geosão nós de command — a fronteiraenrich_grpc_set.geo_ué um nó derivado produzido pela função SQLUPPER.
A fronteira do command não é opaca. Como enrich_grpc_set declara suas colunas de entrada
(id, region) e colunas de saída (id, embedding, geo), o motor de lineage emenda o
fechamento de contaminação continuamente das colunas declaradas da relação de fonte até cada saída.
[tool-verified: _splice_commands and _input_relation in graph.py:245-271]
Tipos de nó e pistas visuais¶
[tool-verified: LineageDag.tsx:25-29, KIND_COLOR constants; LineagePage.tsx:21-26 LEGEND]
| Tipo de nó | Cor | Significado |
|---|---|---|
source |
Verde | Uma coluna de tabela base |
derived |
Azul | Produzida por uma expressão SQL (função, operador, CTE) |
command |
Roxo | Uma coluna de saída de um command registrado |
Anéis adicionais em um nó:
- Anel laranja — uma coluna de saída final da declaração.
- Borda dupla — a relação da coluna é uma view materializada (snapshot MV/CTAS).
- Anel vermelho — membro de um ciclo classificado como erro.
- Anel amarelo — membro de um ciclo classificado como loop de feedback.
[tool-verified: LineageDag.tsx:88-103 Cytoscape style selectors]
Transformações nomeadas em arestas¶
Toda aresta carrega a expressão SQL bruta que produz a coluna alvo, mais uma lista de operações
nomeadas: funções SQL (sql_function), operadores aritméticos/lógicos (operator), commands
registrados (command), referências de coluna simples (identity), e literais (constant).
[tool-verified: TransformOp and name_transform in graph.py:36-145]
Uma aresta de uma chamada de command é renderizada como uma linha roxa tracejada na UI. [tool-verified: LineageDag.tsx:122-124]
Grafo federation-wide (REQ-1161)¶
O grafo de federação mescla o lineage por declaração de toda MV registrada em um único grafo de
proveniência. A identidade do nó é relation.column — a coluna de saída de uma view e a
referência de entrada de outra view para a mesma coluna colapsam em um nó. O resultado é um único
DAG das colunas de fonte base até todo dataset derivado na plataforma. [tool-verified: build_federation_graph in merge.py:205-229
and qualify_outputs in graph.py:275-299]
Use focus, direction, e depth para delimitar a view em escala de federação sem recomputar
o grafo. [tool-verified: slice_graph in merge.py:160-189]
Ciclos (REQ-1161)¶
Ciclos são descritos, não rejeitados. O motor de lineage detecta todo ciclo direcionado e o
classifica. [tool-verified: Cycle.classification property in merge.py:43-46]
| Classificação | Cor da borda | Significado |
|---|---|---|
feedback |
Amarelo | O ciclo atravessa um nó materializado — um loop de feedback legal e defasado no tempo. O snapshot da MV é a fronteira de versão que o torna bem definido. |
error |
Vermelho | Nenhuma fronteira de materialização no loop — uma definição circular sem ordem de avaliação estável. Provavelmente um erro de design. |
[tool-verified: LineagePage.tsx:83-98 cycle alert rendering; merge.py:38-48]
Um ciclo feedback não é uma falha. Uma MV de enriquecimento que realimenta uma coluna derivada de
volta à sua própria relação de fonte é um padrão válido desde que um nó no loop seja materializado
— o snapshot isola as duas metades temporalmente. Um ciclo error precisa de julgamento do
operador: geralmente significa que duas views se referenciam mutuamente sem um snapshot entre elas.
API¶
Ambos os endpoints são estáticos — eles leem definições e contratos, não dados.
POST /admin/lineage/graph¶
Retorna o DAG em nível de coluna para uma única declaração SQL.
POST /admin/lineage/graph
Content-Type: application/json
{
"sql": "SELECT o.id, e.embedding FROM orders o JOIN enrich_grpc_set('main.public.orders') e ON o.id = e.id",
"dialect": "postgres"
}
[tool-verified: lineage_graph endpoint at lineage_router.py:45-54, LineageGraphRequest model at
lineage_router.py:29-31]
Formato da resposta [tool-verified: LineageGraph.to_dict in graph.py:82-105]:
{
"nodes": [
{"id": "orders.id", "column": "id", "relation": "orders", "kind": "source", "materialized": false}
],
"edges": [
{
"source": "orders.id",
"target": "e.id",
"transform": "enrich_grpc_set(...)",
"ops": [{"name": "enrich_grpc_set", "kind": "command"}]
}
],
"outputs": ["id", "embedding"]
}
Retorna HTTP 422 quando o SQL não pode ser analisado. [tool-verified: lineage_router.py:51-54]
GET /admin/lineage/federation¶
Retorna o grafo de proveniência mesclado sobre todas as MVs no registro.
GET /admin/lineage/federation
GET /admin/lineage/federation?focus=orders.id&direction=downstream&depth=3
[tool-verified: federation_graph endpoint at lineage_router.py:73-98]
Parâmetros de consulta [tool-verified: function signature at lineage_router.py:73-76]:
| Parâmetro | Valores | Padrão | Efeito |
|---|---|---|---|
focus |
Um id de nó | — | Delimita a resposta ao subgrafo ao redor deste nó |
direction |
upstream | downstream | both |
both |
Qual direção percorrer a partir de focus |
depth |
inteiro | ilimitado | Distância máxima de hop a partir de focus |
A resposta tem o mesmo formato do grafo de declaração, com um campo cycles adicionado
[tool-verified: MergedGraph.to_dict in merge.py:60-64]:
{
"nodes": [...],
"edges": [...],
"outputs": [...],
"cycles": [
{
"nodes": ["orders.region", "enriched_orders.region"],
"has_materialization_boundary": true,
"classification": "feedback"
}
]
}
O que uma renomeação ou remoção de coluna quebraria (REQ-1484)¶
Uma coluna carrega dois nomes, e cada um é armazenado por um conjunto diferente de artefatos.
O nome exposto é o que as superfícies SQL e GraphQL mostram: table_columns.alias, recaindo
para o padrão snake_case quando nenhum alias está definido [tool-verified: computed_sql_alias at
schema_helpers.py:317]. Views, views materializadas, expressões de métrica, predicados RLS,
contratos DQ, grãos de metric-view e chaves de linha de MV são todos definidos contra esse nome,
então renomear um alias os quebra tão certamente quanto excluir a coluna.
O nome físico é table_columns.column_name, a identidade que sobrevive à substituição
completa de colunas do upsert de tabela. Relacionamentos, vínculos de glossário, atribuições de
tag, a coluna de watermark e os presets de coluna armazenam este, então eles só quebram quando a
coluna é removida.
columnDependents reporta ambos. Views e MVs a jusante vêm de fatiar o grafo de federação no nome
exposto da coluna; os artefatos que esse grafo não cobre vêm de uma varredura direta do registro
[tool-verified: graph_dependents in provisa/lineage/dependents.py, registry scans in
provisa/api/admin/column_dependents.py].
query {
columnDependents(tableId: "42", renamed: ["order_total"], removed: ["legacy_code"]) {
columnName
dependents { kind name detail breaksOn }
}
}
breaksOn é rename para uma referência de nome exposto e remove para uma de nome físico, então
quem chama consegue identificar qual metade da edição cada artefato está reagindo.
Faça essa pergunta antes de salvar. Uma coluna renomeada é localizada pelo nome exposto que ainda carrega no registro; uma vez que o alias foi aplicado, o nome antigo se foi e a consulta não encontra nada.
A página Tables roda a consulta automaticamente quando uma edição pendente muda um alias ou reduz
o conjunto de colunas, e lista o que encontra [tool-verified: diffEditedColumns in
provisa-ui/src/pages/tables/columnDiff.ts, dialog in TablesPage.tsx]. O aviso é consultivo:
nomeia os artefatos afetados e o administrador decide. Ele não bloqueia o salvamento, porque nem
todos os consumidores do estado podem ser alcançados — um dashboard externo ou uma aplicação
cliente que consulta a coluna pelo nome está além do conhecimento do registro. Pela mesma razão,
varreduras sobre texto SQL livre casam a coluna como um token identificador em vez de resolver
escopo, o que pode nomear um artefato que acaba não usando a coluna. Superestimar é a direção
segura para um aviso.
Usando lineage para governar contratos de command¶
Como o fechamento de contaminação conecta toda coluna de entrada declarada a toda coluna de saída declarada, a amplitude desse fechamento depende inteiramente do que você declara.
Considere um command que recebe uma tabela orders completa (id, region, amount,
customer_id, discount, notes, ...) e retorna um embedding. Se o contrato de entrada lista
todas essas colunas, toda coluna a jusante que usa o embedding mostrará lineage de todas elas.
Isso é correto mas não útil — é difícil dizer o que realmente importou.
Declare somente id e text (as colunas que o modelo de embedding realmente lê), e o cone de
lineage se estreita para essas duas colunas de fonte. A derivação é tanto correta quanto precisa.
Veja Commands para a mecânica de declarar um contrato de entrada estreito.