Ir para o conteúdo

Lineage em Nível de Coluna

O Provisa rastreia a linhagem de dados no nível de coluna de forma estática. Ele a calcula a partir das definições SQL e dos contratos de comandos, então nada precisa rodar. Você tem duas visões: um DAG por instrução e um grafo de proveniência de toda a federação, que abrange cada view e view materializada (MV) registrada.

O explorador de lineage

Abra 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.

DAG em nível de declaração

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 e orders.geo são nós de fonte (o contrato de entrada estreito de enrich_grpc_set declara id e region; o fechamento de contaminação completo conecta todas as entradas declaradas a todas as saídas).
  • e.embedding e e.geo são nós de command na fronteira enrich_grpc_set.
  • geo_u é um nó derivado produzido pela função SQL UPPER.

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.

Tipos de nó e pistas visuais

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 exibição materializada (snapshot MV/CTAS).
  • Anel vermelho — membro de um ciclo classificado como erro.
  • Anel amarelo — membro de um ciclo classificado como laço de realimentação.

Transformações nomeadas nas arestas

Toda aresta carrega a expressão SQL bruta que produz a coluna de destino, mais uma lista de operações nomeadas: funções SQL (sql_function), operadores aritméticos/lógicos (operator), commands registrados (command), referências simples de coluna (identity) e literais (constant).

Uma aresta vinda de uma chamada de command é renderizada como uma linha roxa tracejada na UI.

Grafo abrangendo toda a federação

O grafo da federação junta a linhagem por instrução de cada MV registrada num único grafo de proveniência. A identidade de um nó é relation.column, então a coluna de saída de uma view e a referência de entrada de outra view a essa coluna se fundem num nó. O resultado é um único DAG, das colunas das fontes base até cada conjunto de dados derivado da plataforma.

Use focus, direction e depth para delimitar a exibição em escala de federação sem recomputar o grafo.

Ciclos

O motor de linhagem detecta cada ciclo dirigido e o classifica. Um ciclo é descrito, nunca rejeitado.

Classificação Cor da borda Significado
feedback Amarelo O ciclo cruza um nó materializado — um laço de realimentação legítimo 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 laço — uma definição circular sem ordem de avaliação estável. Provavelmente um erro de projeto.

Um ciclo feedback é um padrão válido. Uma MV de enriquecimento que devolve uma coluna derivada à sua própria relação de origem funciona desde que um nó do laço esteja materializado, porque o snapshot separa as duas metades no tempo. Um ciclo error precisa do julgamento de um operador. Em geral significa que duas views se referenciam sem um snapshot no meio.

API

Os dois endpoints são estáticos. Eles leem definições e contratos, nunca 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"
}

Formato da resposta:

{
  "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.

GET /admin/lineage/federation

Retorna o grafo de proveniência fundido sobre todas as MVs no registro.

GET /admin/lineage/federation
GET /admin/lineage/federation?focus=orders.id&direction=downstream&depth=3

Parâmetros de consulta:

Parâmetro Valores Padrão Efeito
focus Um id de nó — Delimita a resposta ao subgrafo em torno deste nó
direction upstream | downstream | both both Qual direção percorrer a partir de focus
depth inteiro sem limite Distância máxima em saltos a partir de focus

A resposta tem o mesmo formato do grafo de declaração, com um campo cycles adicionado :

{
  "nodes": [...],
  "edges": [...],
  "outputs": [...],
  "cycles": [
    {
      "nodes": ["orders.region", "enriched_orders.region"],
      "has_materialization_boundary": true,
      "classification": "feedback"
    }
  ]
}

De que cada atualização foi construída

Cada atualização de uma visão materializada registra de que foi construída: a versão da definição da visão, a versão das suas entradas e como essa versão foi estabelecida, e as colunas de origem e a expressão de cada coluna de saída. A visão operacional "ops"."mv_refresh_lineage" retorna uma linha por atualização:

Coluna Contém
mv_id A visão materializada atualizada
target_table A tabela que a atualização gravou
definition_version A versão da definição da visão
input_version, input_version_kind A versão das suas entradas: um snapshot do Iceberg, uma marca d'água, um token de atualidade ou o horário da atualização
trace_id O trace da atualização em "ops"."traces"
refreshed_at Quando rodou
column_derivations Cada coluna de saída com suas colunas de origem e sua expressão, em JSON
SELECT refreshed_at, definition_version, input_version, input_version_kind
FROM "ops"."mv_refresh_lineage"
WHERE mv_id = 'view-view_exposure'
ORDER BY refreshed_at

A visão lê os registros de atualização em "ops"."traces" e responde em todo motor que os guarda. No Trino são as tabelas Iceberg do catálogo otel. No DuckDB o servidor grava os próprios registros de trace como arquivos Parquet na sua pasta de telemetria (PROVISA_TELEMETRY_DIR, ~/.provisa/telemetry por padrão), um arquivo por lote exportado numa pasta por dia. A compactação agendada da telemetria (otel.compact_cron) junta os arquivos pequenos de cada dia em arquivos de até 64 MB. Cada organização lê só as próprias atualizações, e os registros são mantidos indefinidamente. Os spans só são registrados enquanto o SDK do OpenTelemetry está habilitado (OTEL_SDK_DISABLED não definido ou false).

Uma view como estava, e como foi registrada

Uma view declarada bitemporal guarda cada versão que já teve: cada atualização acrescenta, e nenhuma versão é reescrita, em qualquer armazenamento. Uma leitura simples responde com o estado atual. Dois cabeçalhos da requisição leem o seu passado:

  • X-Provisa-As-Of lê a view numa data, ou sobre um intervalo, no seu relógio: o tempo de alteração da própria fonte quando a view declara bitemporal_source_time, o tempo de carga do Provisa quando não declara.
  • X-Provisa-As-Recorded lê a view como o Provisa a tinha registrado num momento. Versões registradas depois ficam de fora, de modo que uma correção registrada mais tarde não muda a reprodução do passado.

Em 30 de setembro e registrada em 1º de outubro dá o valor original; registrada em 5 de outubro dá a correção. O registro de auditoria de cada instrução guarda os dois tempos (as_of, as_recorded), para que um relatório possa ser reproduzido como foi produzido da primeira vez. Views com histórico descreve como declarar uma view bitemporal e cada superfície que aceita os dois valores.

O que uma renomeação ou remoção de coluna quebraria

Uma coluna carrega dois nomes, e cada um é armazenado por um conjunto diferente de artefatos.

O nome exposto é o que as interfaces SQL e GraphQL mostram: table_columns.alias, recaindo para o padrão snake_case quando nenhum alias está definido. Exibições, exibições materializadas, expressões de métrica, predicados de RLS, contratos de DQ, grãos de exibição de métrica e chaves de linha de MV são todos escritos contra aquele 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 integral de colunas feita pelo upsert da tabela. Relacionamentos, vínculos de glossário, atribuições de tag, a coluna de watermark e presets de coluna guardam este, então eles só quebram quando a coluna é removida.

columnDependents reporta ambos. Exibições e MVs a jusante vêm de fatiar o grafo de federação no nome exposto da coluna; os artefatos que o grafo não cobre vêm de uma varredura direta do registro.

query {
  columnDependents(tableId: "42", renamed: ["order_total"], removed: ["legacy_code"]) {
    columnName
    dependents { kind name detail breaksOn }
  }
}

breaksOn é rename para uma referência ao nome exposto e remove para uma ao nome físico, de modo que quem chama consegue dizer a qual metade da edição cada artefato está reagindo.

Pergunte isto antes de salvar. Uma coluna renomeada é localizada pelo nome exposto que ela ainda carrega no registro; uma vez que o alias tenha sido aplicado, o nome antigo se foi e a consulta não encontra nada.

A página de tabelas roda a consulta automaticamente quando uma edição pendente muda um alias ou reduz o conjunto de colunas, e lista o que encontra. O aviso é consultivo. Ele nomeia os artefatos afetados, e o administrador decide. Ele nunca bloqueia o salvamento, porque não dá para alcançar todos os consumidores do patrimônio: um dashboard externo ou uma aplicação cliente que consulta a coluna pelo nome está fora do conhecimento do registro. Pelo mesmo motivo, varreduras em texto SQL livre casam a coluna como token identificador sem resolver o escopo, então podem nomear um artefato que acaba não usando a coluna. Num aviso, relatar demais é a direção segura.

Usando lineage para governar contratos de command

Como o fechamento de contaminação conecta cada coluna de entrada declarada a cada coluna de saída declarada, a amplitude desse fechamento depende inteiramente do que você declara.

Pense num comando que recebe uma tabela de pedidos completa (id, region, amount, customer_id, discount, notes, ...) e devolve um embedding. Se o contrato de entrada lista todas essas colunas, cada coluna a jusante que usa o embedding mostra linhagem de todas elas. Isso é exato, mas não ajuda, porque você não consegue dizer o que de fato importou.

Declare somente id e text (as colunas que o modelo de embedding de fato lê), e o cone de lineage se aperta para essas duas colunas de fonte. A derivação é ao mesmo tempo correta e precisa.

Veja Commands para a mecânica de declarar um contrato de entrada estreito.