Data Lineage
Ontul automatically tracks data lineage — which tables and columns a table was derived from — at both table and column granularity. Lineage is extracted from the actual Calcite query plan, not from heuristic SQL parsing, so joins, sub-queries, CTEs, and aggregations are resolved accurately.
Automatic Capture
When a table is created or populated, Ontul records its lineage — no configuration, no annotations. Capture happens on:
CREATE TABLE ... AS SELECT(CTAS)INSERT INTO ... SELECTMERGE INTOCREATE VIEWSUBMIT STREAMING— an Ontul Flow: the flow's source(s) → sink edge is recorded when the flow starts
Capture is best-effort: it runs after the statement succeeds and never blocks or fails the query.
For a streaming flow, Ontul-catalog tables appear under their catalog.schema.table name (so a flow's
edge lines up with SQL-DML lineage), while external systems are recorded as scheme-prefixed datasets —
kafka://<topic>, cdc://<connector>/<table>, jdbc://<table>, es://<index>, http://<endpoint>,
neorunbase://<table>, or the S3 path for a file source.
How Repeated Writes Accumulate
A table is often fed by more than one statement — a CTAS from one engine, then INSERTs from
another. Table-level lineage keeps one entry per target table, and each new edge is combined with
what is already there according to what the statement means for the target:
| Operation (as recorded) | Produced by | Effect on the target's sources |
|---|---|---|
CTAS |
CREATE TABLE … AS SELECT (Ontul or an engine plugin) |
Replaces them — the table is defined afresh |
VIEW |
CREATE [OR REPLACE] VIEW |
Replaces them — a redefined view drops its old sources |
semantic-view |
Semantic-layer view registration | Replaces them |
INSERT_INTO, INSERT |
INSERT INTO … SELECT (Ontul / engine plugin) |
Adds the new sources |
MERGE_INTO |
MERGE INTO |
Adds the new sources |
STREAMING |
SUBMIT STREAMING (Ontul Flow start) |
Adds the flow's sources |
ACTION |
Ontology action invocation | Adds |
EXTERNAL (default) or any other value sent |
POST /v1/api/lineage |
Adds — only CTAS, VIEW and semantic-view replace |
Two further rules keep the graph honest:
- A table reading itself is not an edge.
INSERT INTO t SELECT … FROM t(de-duplication, backfill from its own history) does not recordt ← t. - The label belongs to the statement that introduced the source. The stored operation labels the
edges in the graph. A statement that adds no new source — such as the self-
INSERTabove — leaves the existing label (for exampleCTAS) in place instead of relabelling every edge asINSERT_INTO.
For example:
-- Trino (via the chango plugin): CREATE TABLE ice.s.orders AS SELECT … FROM tpch.tiny.orders
-- Ontul: INSERT INTO ice.s.orders SELECT … FROM pg.public.orders_backfill
-- Ontul: INSERT INTO ice.s.orders SELECT … FROM ice.s.orders WHERE …
SHOW LINEAGE ice.s.orders;
-- tpch.tiny.orders
-- pg.public.orders_backfill
Column-level lineage is stored per target and reflects the most recent statement that wrote the table.
Table-Level Lineage
Table-level lineage records the set of source tables a table was built from — every table scanned by the defining query, across joins and unions.
CREATE TABLE iceberg_catalog.demo.cust_orders AS
SELECT c.c_name,
count(o.o_orderkey) AS order_count,
sum(o.o_totalprice) AS total_spend
FROM tpch.tiny.customer c
JOIN tpch.tiny.orders o ON c.c_custkey = o.o_custkey
GROUP BY c.c_name;
Column-Level Lineage
For each output column, Ontul records the source (table, column) it derives from. A column that passed through an expression — SUM(...), CAST(...), an aggregate — is flagged derived; a straight projection of a base column is not. Column origins come from Calcite column-origin metadata, so they are tracked correctly through joins and GROUP BY.
SHOW LINEAGE COLUMNS iceberg_catalog.demo.cust_orders;
-- c_name ← tpch.tiny.customer.c_name
-- order_count ← tpch.tiny.orders.o_orderkey [derived]
-- total_spend ← tpch.tiny.orders.o_totalprice [derived]
SQL Commands
| Command | Result |
|---|---|
SHOW LINEAGE <table> |
Upstream source tables |
SHOW LINEAGE COLUMNS <table> |
Column-level lineage edges |
SHOW LINEAGE DOWNSTREAM <table> |
Tables built from this one |
REST API
Read-only lineage endpoints (any authenticated user):
| Endpoint | Returns |
|---|---|
GET /admin/lineage/graph?root=<table>&depth=<n>&direction=<up\|down\|both> |
Lineage graph {nodes, edges} |
GET /admin/lineage/columns?table=<table> |
Column-level lineage |
GET /admin/lineage/impact?table=<table> |
Transitive downstream tables |
GET /admin/lineage/pii-flow?table=<table>&column=<col> |
Downstream flow of a column |
External-Engine Lineage Ingestion
Ontul is not only the source of lineage — it is also a collection point for other engines. Trino, Spark, and Flink authorization plugins (as deployed by Chango) already route their access-control decisions through Ontul IAM; the same plugins push the lineage edges they observe into Ontul over a REST endpoint, so lineage from every engine lands in one replicated store.
The endpoint lives at the server root, not under /admin:
POST /v1/api/lineage
Authorization: Token <ontul-token> # or: Bearer <jwt> / AccessKey
Content-Type: application/json
{
"target": "lake.curated.orders_daily",
"sources": ["hive.sales.orders", "hive.sales.customers"],
"operation": "INSERT"
}
Response: {"status":"ok"}; target is required (400 otherwise). operation defaults to
EXTERNAL. The plugins send CTAS for CREATE TABLE … AS and INSERT for inserts, so the
accumulation rules apply to their edges exactly as to Ontul's.
The ingested edge is stored and queried exactly like a natively-captured one — it appears in SHOW LINEAGE, the graph/impact/PII-flow endpoints, and the Admin UI graph, regardless of which engine produced it. This is the lineage half of Ontul's cross-engine Audit Log; the audit half records who / which engine / which query for the same operation.
Admin UI
The Data Lineage page renders an interactive graph (sources on the left, derived tables on the right). Enter a table, choose direction and depth, and trace its lineage. Click any node to open a side panel showing its column-level lineage — with the derived flag — and its downstream impact.
Each edge is labelled with the operation that introduced it (CTAS, INSERT_INTO, MERGE_INTO, VIEW, …). The page can be opened directly by URL (/admin/lineage) or reloaded.
The Audit → Overview & Graph page shows a different, complementary graph — user → engine → table built from audit events — see Audit Log.
PII Flow Detection
Given a sensitive column, Ontul traces every downstream (table, column) it transitively flows into, via column-level lineage:
This answers "where did this PII go?" — for example, finding that users.ssn was copied by an ETL job into a table that has no access policy. Ontul surfaces the flow for an operator to review; it does not automatically modify IAM policies, since lineage is not exhaustive (opaque UDFs, dynamic SQL) and silent policy changes would be surprising.
Cross-Catalog Lineage
Because every catalog's tables — Iceberg, JDBC, Kafka, TPC-H — are first-class scan sources, lineage crosses systems automatically. A table built FROM jdbc_catalog.public.users JOIN kafka_catalog.events.clicks records both as upstream sources, giving cross-system lineage with no extra configuration.
OpenLineage Emission
Ontul can emit OpenLineage-spec events so external catalogs (Marquez, DataHub, Atlan, …) ingest Ontul lineage through the industry-standard wire format.
Set ontul.lineage.openlineage.url to an HTTP endpoint; each CTAS/INSERT/MERGE/VIEW — and each streaming Ontul Flow start — then emits a COMPLETE RunEvent with inputs, outputs, and a standard columnLineage facet. When unset, emission is disabled.
Storage
Lineage is persisted in the leader's metadata store and replicated to followers — the same mechanism used for catalogs and IAM — so it survives restarts and leadership changes. An edge recorded on a follower master (the statement or Flow start was routed there) is forwarded to the leader and replicated back, the same way as audit records.