Derivazione a livello di colonna¶
Provisa tiene traccia della derivazione dei dati a livello di colonna in modo statico — calcolata a partire dalle definizioni SQL e dai contratti di comando, senza necessità di esecuzione. Sono disponibili due viste: un DAG per singola istruzione e un grafo di provenienza a livello di federazione che copre tutte le viste e le viste materializzate (MV) registrate.
L'esploratore di derivazione¶
Accedere a Lineage nell'interfaccia utente (/lineage). Incollare un'istruzione SQL e fare clic
su Build statement graph per visualizzarne il DAG a livello di colonna. Fare clic su
Federation graph per caricare il grafo di provenienza su ogni MV nel registro.
[tool-verified: LineagePage.tsx:28-119]
DAG a livello di istruzione (REQ-1160)¶
Ogni colonna di output denominata nel proprio SQL diventa un nodo. Il generatore la ripercorre attraverso ogni CTE, sottoquery, join e chiamata di comando inline fino alle colonne di origine, costruendo un grafo diretto dagli input di origine agli output finali.
Esempio svolto¶
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
Questa istruzione produce tre colonne di output. Il grafo per geo_u è il seguente:
orders.geo ──[enrich_grpc_set(...)]──► e.geo ──[UPPER]──► geo_u
orders.id ─╮ (taint closure)
orders.region ─╯
orders.id,orders.regioneorders.geosono nodi source (il contratto di input ristretto dienrich_grpc_setdichiaraideregion; la taint closure completa collega tutti gli input dichiarati a tutti gli output). [tool-verified:_splice_commandsin graph.py:223-242]e.embeddingede.geosono nodi command — il confine dienrich_grpc_set.geo_uè un nodo derived prodotto dalla funzione SQLUPPER.
Il confine del comando non è opaco. Poiché enrich_grpc_set dichiara le proprie colonne di
input (id, region) e di output (id, embedding, geo), il motore di derivazione collega la
taint closure in modo continuo dalle colonne dichiarate della relazione di origine fino a ciascun
output. [tool-verified: _splice_commands e _input_relation in graph.py:245-271]
Tipi di nodo e indizi visivi¶
[tool-verified: LineageDag.tsx:25-29, KIND_COLOR constants; LineagePage.tsx:21-26 LEGEND]
| Tipo di nodo | Colore | Significato |
|---|---|---|
source |
Verde | Una colonna di una tabella di base |
derived |
Blu | Prodotta da un'espressione SQL (funzione, operatore, CTE) |
command |
Viola | Una colonna di output di un comando registrato |
Anelli aggiuntivi su un nodo:
- Anello arancione — una colonna di output finale dell'istruzione.
- Bordo doppio — la relazione della colonna è una vista materializzata (snapshot MV/CTAS).
- Anello rosso — membro di un ciclo classificato come errore.
- Anello giallo — membro di un ciclo classificato come ciclo di retroazione (feedback loop).
[tool-verified: LineageDag.tsx:88-103 Cytoscape style selectors]
Trasformazioni denominate sugli archi¶
Ogni arco riporta l'espressione SQL grezza che produce la colonna di destinazione, oltre a un
elenco di operazioni denominate: funzioni SQL (sql_function), operatori aritmetici/logici
(operator), comandi registrati (command), riferimenti di colonna semplici (identity) e
letterali (constant). [tool-verified: TransformOp and name_transform in graph.py:36-145]
Un arco proveniente da una chiamata di comando viene rappresentato come una linea viola tratteggiata nell'interfaccia utente. [tool-verified: LineageDag.tsx:122-124]
Grafo a livello di federazione (REQ-1161)¶
Il grafo di federazione unisce la derivazione per singola istruzione di ogni MV registrata in un
unico grafo di provenienza. L'identità del nodo è relation.column — la colonna di output di una
vista e il riferimento di input di un'altra vista alla stessa colonna collassano in un unico nodo.
Il risultato è un unico DAG dalle colonne di origine di base fino a ogni dataset derivato nella
piattaforma. [tool-verified: build_federation_graph in merge.py:205-229 e qualify_outputs in
graph.py:275-299]
Utilizzare focus, direction e depth per delimitare la vista su scala di federazione senza
ricalcolare il grafo. [tool-verified: slice_graph in merge.py:160-189]
Cicli (REQ-1161)¶
I cicli vengono descritti, non rifiutati. Il motore di derivazione rileva ogni ciclo diretto e lo
classifica. [tool-verified: Cycle.classification property in merge.py:43-46]
| Classificazione | Colore del bordo | Significato |
|---|---|---|
feedback |
Giallo | Il ciclo attraversa un nodo materializzato — un ciclo di retroazione legittimo e sfasato nel tempo. Lo snapshot della MV costituisce il confine di versione che lo rende ben definito. |
error |
Rosso | Nessun confine di materializzazione sul ciclo — una definizione circolare senza un ordine di valutazione stabile. Probabilmente un errore di progettazione. |
[tool-verified: LineagePage.tsx:83-98 cycle alert rendering; merge.py:38-48]
Un ciclo feedback non è un errore. Una MV di arricchimento che reinserisce una colonna derivata
nella propria relazione di origine è un pattern valido, purché un nodo del ciclo sia materializzato
— lo snapshot isola temporalmente le due metà. Un ciclo error richiede il giudizio di un
operatore: di norma significa che due viste si fanno riferimento reciprocamente senza uno snapshot
intermedio.
API¶
Entrambi gli endpoint sono statici — leggono definizioni e contratti, non dati.
POST /admin/lineage/graph¶
Restituisce il DAG a livello di colonna per una singola istruzione 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]
Forma della risposta [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"]
}
Restituisce HTTP 422 quando l'SQL non può essere analizzato. [tool-verified: lineage_router.py:51-54]
GET /admin/lineage/federation¶
Restituisce il grafo di provenienza unificato su tutte le MV nel 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]
Parametri di query [tool-verified: function signature at lineage_router.py:73-76]:
| Parametro | Valori | Predefinito | Effetto |
|---|---|---|---|
focus |
Un id di nodo | — | Delimita la risposta al sottografo attorno a questo nodo |
direction |
upstream | downstream | both |
both |
Direzione di attraversamento a partire da focus |
depth |
intero | illimitato | Distanza massima in hop da focus |
La risposta ha la stessa forma del grafo di istruzione, con l'aggiunta di un campo cycles
[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"
}
]
}
Cosa romperebbe la rinomina o l'eliminazione di una colonna (REQ-1484)¶
Una colonna porta due nomi, e ciascuno è memorizzato da un insieme diverso di artefatti.
Il nome esposto è ciò che le superfici SQL e GraphQL mostrano: table_columns.alias, con
fallback al valore predefinito snake_case quando non è impostato alcun alias [tool-verified:
computed_sql_alias at schema_helpers.py:317]. Viste, viste materializzate, espressioni di
metrica, predicati RLS, contratti DQ, grani delle metric-view e chiavi di riga delle MV sono tutti
scritti in riferimento a quel nome, quindi rinominare un alias li rompe con la stessa certezza
con cui li romperebbe eliminare la colonna.
Il nome fisico è table_columns.column_name, l'identità che sopravvive alla sostituzione
integrale delle colonne durante l'upsert della tabella. Relazioni, associazioni al glossario,
assegnazioni di tag, la colonna watermark e i preset di colonna memorizzano questo nome, quindi si
rompono solo quando la colonna viene rimossa.
columnDependents riporta entrambi. Le viste e le MV a valle provengono dal sezionamento del
grafo di federazione in corrispondenza del nome esposto della colonna; gli artefatti che quel
grafo non copre provengono da una scansione diretta del 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 vale rename per un riferimento al nome esposto e remove per uno al nome fisico,
così un chiamante può capire a quale metà della modifica reagisce ciascun artefatto.
Effettuare questa richiesta prima del salvataggio. Una colonna rinominata viene individuata tramite il nome esposto che ancora porta nel registro; una volta che l'alias è stato applicato, il vecchio nome scompare e la query non trova nulla.
La pagina Tables esegue automaticamente la query quando una modifica in sospeso cambia un alias o
riduce l'insieme di colonne, ed elenca ciò che trova [tool-verified: diffEditedColumns in
provisa-ui/src/pages/tables/columnDiff.ts, dialog in TablesPage.tsx]. L'avviso è consultivo:
indica gli artefatti interessati e la decisione spetta all'amministratore. Non blocca il
salvataggio, perché non tutti i consumer dell'estate sono raggiungibili — una dashboard esterna o
un'applicazione client che interroga la colonna per nome è al di fuori della conoscenza del
registro. Per lo stesso motivo, le scansioni su testo SQL libero fanno corrispondere la colonna
come token identificatore anziché risolvere lo scope, il che può indicare un artefatto che in
realtà non utilizza la colonna. Nella direzione della sicurezza, un avviso preferisce segnalare
troppo piuttosto che troppo poco.
Utilizzare la derivazione per governare i contratti di comando¶
Poiché la taint closure collega ogni colonna di input dichiarata a ogni colonna di output dichiarata, l'ampiezza di tale closure dipende interamente da ciò che si dichiara.
Si consideri un comando che riceve una tabella orders completa (id, region, amount,
customer_id, discount, notes, ...) e restituisce un embedding. Se il contratto di input
elenca tutte queste colonne, ogni colonna a valle che utilizza l'embedding mostrerà la derivazione
da tutte quante. Ciò è corretto ma poco utile — è difficile capire cosa abbia effettivamente
contato.
Dichiarando solo id e text (le colonne che il modello di embedding legge realmente), il cono di
derivazione si restringe a queste due colonne di origine. La derivazione risulta così al tempo
stesso corretta e precisa.
Vedere Commands per la meccanica di dichiarazione di un contratto di input ristretto.