Column-Level Lineage¶
Provisa erfasst Column-Level Data Lineage statisch — berechnet aus SQL-Definitionen und Command-Contracts, ohne dass eine Ausführung erforderlich ist. Zwei Ansichten stehen zur Verfügung: ein DAG pro Statement und ein föderationsweiter Provenance-Graph über alle registrierten Views und materialisierten Views (MVs).
Der Lineage-Explorer¶
Navigieren Sie in der UI zu Lineage (/lineage). Fügen Sie ein SQL-Statement ein und klicken
Sie auf Build statement graph, um dessen Column-Level-DAG zu sehen. Klicken Sie auf
Federation graph, um den Provenance-Graphen über alle MVs in der Registry zu laden.
[tool-verified: LineagePage.tsx:28-119]
DAG auf Statement-Ebene (REQ-1160)¶
Jede benannte Ausgabespalte in Ihrem SQL wird zu einem Knoten. Der Builder verfolgt sie durch jede CTE, Subquery, jeden Join und jeden Inline-Command-Aufruf zurück bis zu ihren Quellspalten und baut so einen gerichteten Graphen von Quelleingaben zu den finalen Ausgaben.
Durchgerechnetes Beispiel¶
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
Dieses Statement erzeugt drei Ausgabespalten. Der Graph für geo_u sieht so aus:
orders.geo ──[enrich_grpc_set(...)]──► e.geo ──[UPPER]──► geo_u
orders.id ─╮ (taint closure)
orders.region ─╯
orders.id,orders.regionundorders.geosind source-Knoten (der schmale Eingabe-Contract vonenrich_grpc_setdeklariertidundregion; die vollständige Taint-Closure verbindet alle deklarierten Eingaben mit allen Ausgaben). [tool-verified:_splice_commandsin graph.py:223-242]e.embeddingunde.geosind command-Knoten — die Grenze vonenrich_grpc_set.geo_uist ein derived-Knoten, erzeugt durch die SQL-FunktionUPPER.
Die Command-Grenze ist nicht undurchsichtig. Weil enrich_grpc_set seine Eingabespalten
(id, region) und Ausgabespalten (id, embedding, geo) deklariert, verbindet die
Lineage-Engine die Taint-Closure durchgehend von den deklarierten Spalten der Quellrelation bis
zu jeder Ausgabe. [tool-verified: _splice_commands und _input_relation in graph.py:245-271]
Knotenarten und visuelle Hinweise¶
[tool-verified: LineageDag.tsx:25-29, KIND_COLOR constants; LineagePage.tsx:21-26 LEGEND]
| Knotenart | Farbe | Bedeutung |
|---|---|---|
source |
Grün | Eine Basistabellenspalte |
derived |
Blau | Erzeugt durch einen SQL-Ausdruck (Funktion, Operator, CTE) |
command |
Violett | Eine Ausgabespalte eines registrierten Commands |
Zusätzliche Ringe an einem Knoten:
- Oranger Ring — eine finale Ausgabespalte des Statements.
- Doppelter Rahmen — die Relation der Spalte ist eine materialisierte View (MV/CTAS-Snapshot).
- Roter Ring — Mitglied eines als Fehler klassifizierten Zyklus.
- Gelber Ring — Mitglied eines als Feedback-Loop klassifizierten Zyklus.
[tool-verified: LineageDag.tsx:88-103 Cytoscape style selectors]
Benannte Transforms an Kanten¶
Jede Kante trägt den rohen SQL-Ausdruck, der die Zielspalte erzeugt, plus eine Liste benannter
Operationen: SQL-Funktionen (sql_function), arithmetische/logische Operatoren (operator),
registrierte Commands (command), reine Spaltenreferenzen (identity) und Literale (constant).
[tool-verified: TransformOp and name_transform in graph.py:36-145]
Eine Kante aus einem Command-Aufruf wird in der UI als gestrichelte violette Linie dargestellt. [tool-verified: LineageDag.tsx:122-124]
Föderationsweiter Graph (REQ-1161)¶
Der Föderationsgraph führt die Per-Statement-Lineage jeder registrierten MV zu einem einzigen
Provenance-Graphen zusammen. Die Knotenidentität ist relation.column — die Ausgabespalte einer
View und die Eingabereferenz einer anderen View auf dieselbe Spalte fallen zu einem Knoten
zusammen. Das Ergebnis ist ein einziger DAG von Basis-Quellspalten zu jedem abgeleiteten Dataset
in der Plattform. [tool-verified: build_federation_graph in merge.py:205-229 and
qualify_outputs in graph.py:275-299]
Verwenden Sie focus, direction und depth, um die Ansicht auf Föderationsebene einzugrenzen,
ohne den Graphen neu zu berechnen. [tool-verified: slice_graph in merge.py:160-189]
Zyklen (REQ-1161)¶
Zyklen werden beschrieben, nicht abgelehnt. Die Lineage-Engine erkennt jeden gerichteten Zyklus
und klassifiziert ihn. [tool-verified: Cycle.classification property in merge.py:43-46]
| Klassifikation | Randfarbe | Bedeutung |
|---|---|---|
feedback |
Gelb | Der Zyklus durchquert einen materialisierten Knoten — ein legaler, zeitversetzter Feedback-Loop. Der MV-Snapshot ist die Versionsgrenze, die ihn wohldefiniert macht. |
error |
Rot | Keine Materialisierungsgrenze im Loop — eine zirkuläre Definition ohne stabile Auswertungsreihenfolge. Vermutlich ein Designfehler. |
[tool-verified: LineagePage.tsx:83-98 cycle alert rendering; merge.py:38-48]
Ein feedback-Zyklus ist kein Fehler. Eine Enrichment-MV, die eine abgeleitete Spalte zurück in
ihre eigene Quellrelation einspeist, ist ein gültiges Muster, solange ein Knoten im Loop
materialisiert ist — der Snapshot isoliert die beiden Hälften zeitlich. Ein error-Zyklus
erfordert menschliches Urteilsvermögen: Er bedeutet in der Regel, dass sich zwei Views ohne
dazwischenliegenden Snapshot gegenseitig referenzieren.
API¶
Beide Endpunkte sind statisch — sie lesen Definitionen und Contracts, keine Daten.
POST /admin/lineage/graph¶
Liefert den Column-Level-DAG für ein einzelnes SQL-Statement.
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]
Form der Antwort [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"]
}
Liefert HTTP 422, wenn das SQL nicht geparst werden kann. [tool-verified: lineage_router.py:51-54]
GET /admin/lineage/federation¶
Liefert den zusammengeführten Provenance-Graphen über alle MVs in der Registry.
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]
Query-Parameter [tool-verified: function signature at lineage_router.py:73-76]:
| Parameter | Werte | Standard | Effekt |
|---|---|---|---|
focus |
Eine Knoten-ID | — | Beschränkt die Antwort auf den Sub-Graphen um diesen Knoten |
direction |
upstream | downstream | both |
both |
Richtung, in der von focus aus traversiert wird |
depth |
Ganzzahl | unbegrenzt | Maximale Hop-Distanz von focus |
Die Antwort hat dieselbe Form wie der Statement-Graph, mit einem zusätzlichen Feld 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"
}
]
}
Was eine Spaltenumbenennung oder -löschung beeinträchtigen würde (REQ-1484)¶
Eine Spalte trägt zwei Namen, und jeder wird von einer anderen Gruppe von Artefakten gespeichert.
Der exponierte Name ist das, was die SQL- und GraphQL-Oberflächen zeigen: table_columns.alias,
mit Rückfall auf den snake_case-Standard, wenn kein Alias gesetzt ist [tool-verified:
computed_sql_alias at schema_helpers.py:317]. Views, materialisierte Views,
Metrik-Ausdrücke, RLS-Prädikate, DQ-Contracts, Metric-View-Granularitäten und MV-Row-Keys sind
alle gegen diesen Namen geschrieben, sodass das Umbenennen eines Alias sie genauso sicher
bricht wie das Löschen der Spalte.
Der physische Name ist table_columns.column_name, die Identität, die den vollständigen
Spaltenersatz beim Tabellen-Upsert überlebt. Relationships, Glossar-Bindungen,
Tag-Zuweisungen, die Watermark-Spalte und Spalten-Presets speichern diesen, sodass sie nur
brechen, wenn die Spalte entfernt wird.
columnDependents meldet beides. Nachgelagerte Views und MVs stammen aus dem Slicing des
Föderationsgraphen am exponierten Namen der Spalte; die Artefakte, die dieser Graph nicht
abdeckt, stammen aus einem direkten Scan der Registry [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 ist rename für eine Referenz auf den exponierten Namen und remove für eine auf
den physischen Namen, sodass ein Aufrufer erkennen kann, auf welche Hälfte der Änderung jedes
Artefakt reagiert.
Stellen Sie diese Abfrage vor dem Speichern. Eine umbenannte Spalte wird über den exponierten Namen gefunden, den sie in der Registry noch trägt; sobald der Alias übernommen wurde, ist der alte Name verschwunden und die Abfrage findet nichts.
Die Tables-Seite führt die Abfrage automatisch aus, wenn eine ausstehende Änderung einen Alias
ändert oder die Spaltenmenge verkleinert, und listet die Ergebnisse auf [tool-verified:
diffEditedColumns in provisa-ui/src/pages/tables/columnDiff.ts, dialog in TablesPage.tsx].
Die Warnung ist beratend: Sie nennt die betroffenen Artefakte, und der Administrator entscheidet.
Sie blockiert das Speichern nicht, weil nicht alle Verbraucher des Bestands erreichbar sind — ein
externes Dashboard oder eine Client-Anwendung, die die Spalte namentlich abfragt, liegt außerhalb
des Wissens der Registry. Aus demselben Grund matchen Scans über freien SQL-Text die Spalte als
Bezeichner-Token, statt den Geltungsbereich aufzulösen, was ein Artefakt benennen kann, das die
Spalte am Ende gar nicht verwendet. Überberichten ist bei einer Warnung die sichere Richtung.
Lineage zur Steuerung von Command-Contracts nutzen¶
Weil die Taint-Closure jede deklarierte Eingabespalte mit jeder deklarierten Ausgabespalte verbindet, hängt die Breite dieser Closure vollständig davon ab, was Sie deklarieren.
Betrachten Sie einen Command, der eine vollständige Orders-Tabelle (id, region, amount,
customer_id, discount, notes, ...) entgegennimmt und ein embedding zurückgibt. Listet der
Eingabe-Contract all diese Spalten auf, zeigt jede nachgelagerte Spalte, die das Embedding
verwendet, Lineage von allen davon. Das ist korrekt, aber nicht nützlich — es ist schwer zu
erkennen, was tatsächlich relevant war.
Deklarieren Sie nur id und text (die Spalten, die das Embedding-Modell tatsächlich liest),
und der Lineage-Kegel verengt sich auf diese beiden Quellspalten. Die Herleitung ist sowohl
korrekt als auch präzise.
Siehe Commands für die Mechanik der Deklaration eines schmalen Eingabe-Contracts.