Lineage¶
A semantic model answers "what is revenue?" once; the lineage graph shows where that answer comes from. Dosi projects a compiled model into a graph of four lanes:
| lane | node | edge into it |
|---|---|---|
| physical table | main.orders |
— |
| dataset | orders |
reads_table |
| atomic metric | revenue |
aggregates from each dataset it reads |
| derived metric | net_revenue |
derive_base, compose_member, window_base, window_second from the metrics it builds on |
Datasets are joined by join edges, one per relationship. The graph is a
pure function of the model: the same model always produces the same bytes,
with no timestamps and nothing about the machine that produced it.
Getting it¶
$ dosi lineage dump --model model.yaml > graph.json
$ dosi lineage view --model model.yaml # opens a page in your browser
from dosi_engine import Engine
graph = Engine(model_path="model.yaml").lineage()
graph = Engine(model_path="model.yaml").lineage(redact_sql=True)
view writes one self-contained HTML file — no network access, opens over
file:// — with the lanes left to right, badges on the nodes, edges styled
by kind, a name filter, and a panel that shows a node's detail and lets you
click through its edges. The derived lane is drawn as several columns, one
per step of the derive chain: a derived metric sits one column to the right
of the rightmost metric it is built from, so a compose chain reads left to
right instead of collapsing into one column wired to itself. Without a
browser (--no-open, or a server with no display) it prints the file's
path instead.
redact_sql keeps the shape and replaces every SQL text — dataset sources,
field expressions, metric expressions, derive predicates — and every opaque
extension payload with "<redacted>". Names are never redacted.
What a node carries¶
Every node has an id (table:, dataset:, metric: prefixed), a name,
a label, badges, and a detail block whose shape depends on the kind.
- Dataset: the source (table name, or the query and its pretty-printed
form), the tables it reads, keys, the primary time dimension, and every
field with its expression, the physical columns behind it, and its
label. - Metric: the authored expression and dialect, the datasets and physical columns it reads, the dimensions worth grouping it by, its time dimension, parameters, window and derive declarations, and its semantics — shape, attribution strategy, additivity.
dimensions is what dosi list dimensions --metric
recommends, in the same order: primary and foreign keys, and the columns some
metric aggregates, are left out, because a graph that lists every reachable
field makes the reader sift them out by hand. A name that is not listed is
still groupable — the judgement withholds a recommendation, it never blocks an
explicit group-by.
label is the display name. Apache Ossie defines one for fields
(label on a field, the same property the Databricks, dbt and other
converters read as the display name) and none yet for datasets or metrics,
so a dataset or metric label always equals its name. Dosi never invents a
title: label == name means nobody gave one.
With an ontology¶
Load an Apache Ossie ontology and the graph gains
an ontology block: one node per entity, and edges that tie the concepts to
the lanes below and above them.
| edge | from → to | what it says |
|---|---|---|
mapping |
dataset → concept | the dataset an entity is mapped onto — the one it is identified from, or, for an entity with no table of its own, the datasets its properties are denormalized onto |
relationship |
concept → concept | a navigable relationship between two entities, with its role and multiplicity; realized_by names the join edges it walks |
Both are lineage: where a concept's data is. What an ontology's derived_by
rule says about a metric is semantics, so it rides on the metric node, where
v2 already keeps dimensions and time_dimension:
"concept": {
"path": "Airport.average_departure_delay", // the concept spelling
"measure": "Flight.departure_delay", // the value it aggregates
"group_by": "Airport", // its grain
"via": ["relationship:Flight.departs_from"], // the path that correlates them
"realized_by": ["join:flight_departs_from"]
}
via is the reason the overlay exists. An ontology can derive two metrics
with byte-identical SQL — an airport's average departure delay, and the
average departure delay of the flights arriving at it — that differ only
in which relationship correlates a flight to the airport. In the plain graph
they are indistinguishable; here each names its own path.
path is what describe prints and what query and select accept, so
the graph and the query tools speak the same names. Metrics written by hand
in the model have no concept; their lineage is the aggregates edge, and
the dataset it starts from carries the mapping edge to its entity.
Concept nodes say what their mapping landed on: table (a dataset of its
own, one row per object), denormalized (no table of its own; its columns
sit on another entity's, whose rows they are a projection of), or
unmapped (nothing it declares resolves). The viewer draws the second two
with a dashed border.
The overlay carries no SQL, so redact_sql leaves it unchanged. A model
with no ontology has no ontology key at all — the payload is exactly what
it was before ontologies existed.
Python¶
Engine accepts an ontology in both directions the CLI does:
Engine(ontology_path="flights.yaml") # the embedded model is the model
Engine(model_path="model.yaml", ontology_path="flights.yaml") # the model wins; the ontology supplies concepts
Engine(ontology_text=yaml_text, mapping="prod_mapping") # text, and one of several ontology_mappings
lineage() then includes the overlay. Constructs the ontology declares but
Dosi cannot lower (an n-ary relationship, an unmapped concept) are reported
as RuntimeWarnings at construction, the same channel as compile warnings.
An ontology that cannot be parsed or lowered at all raises
ModelError with code ontology; the ontology options are keyword-only.
See also¶
- CLI reference —
dosi lineage - Semantics contract for the
semanticsblock on metric nodes - Datus extensions for
window,deriveandparams