Inputs and How Lineage Works
Input |
What Konstruct reads |
Status |
|---|---|---|
dbt |
|
v0 |
SQL |
Column-level lineage with sqlglot for dbt models, warehouse views, Airflow SQL and standalone |
v0 |
Airflow |
DAG files parsed statically (never imported): |
v0 |
DuckDB |
Tables, views, columns, types, comments, primary keys and view definitions, read only |
v0 |
Postgres |
Tables, views, materialised views, partitioned tables, columns, types, comments, primary keys and view definitions, read only |
v0 |
Iceberg |
Any PyIceberg catalog, REST included: namespaces, tables, columns (nested struct fields as |
v0 |
Dagster |
Definition files parsed statically (never imported): |
v0 |
PySpark |
Python jobs parsed statically (never imported): files that import PySpark, Databricks notebooks exported as source, and files that call |
v0 |
Looker |
LookML files ( |
v0 |
Power BI |
Semantic models saved as TMDL ( |
v0 |
Loaders |
The tables ingestion tools land, from the marks each leaves in the warehouse: metadata columns, bookkeeping tables or a schema of its own (see Loaders). Each schema a tool lands gets one sync task ( |
v0 |
Loader configuration |
The files that configure loaders, read without running them: dlt pipelines (parsed with |
v0 |
Fivetran lineage |
When a warehouse holds the Fivetran Platform Connector’s lineage tables ( |
v0 |
dlt lineage |
From each dataset’s |
v0 |
Business definitions |
|
v0 |
Vocabularies |
SKOS concepts and OWL classes in Turtle, N-Triples or RDF/XML files named in |
v0 |
Apache Ossie |
|
v0 |
Snowflake |
Tables, views, columns, types, comments and view definitions from each database’s |
v0 |
BigQuery |
Datasets, tables, views and view queries through the client API, with nested |
v0 |
Databricks |
Unity Catalog |
v0 |
Trino |
Tables, views, columns, types (with |
v0 |
Loaders
Konstruct recognises a loader’s tables only by the full set of marks the loader documents, so an application’s own op, deleted or received_at column, or a tracks table of its own, is not taken for a loader’s. A mark that several tools share names the family (singer, segment_spec); a companion table that only one of them leaves in the same schema refines it, and so does a loader the project names for the table in a dbt source’s loader:. A dbt source whose loader: names a tool, with no mark in the warehouse or no warehouse at all, is that tool’s landed table all the same, under the name in lower case with words joined by _ (Fivetran is fivetran).
Loader |
|
What marks its tables in the warehouse |
|---|---|---|
Fivetran |
|
|
Airbyte |
|
|
Singer (Meltano, PipelineWise and other Singer targets) |
|
|
Stitch |
|
The Singer columns, in a schema that also holds |
Meltano |
|
The Singer columns, on a table the project says Meltano lands (a dbt source’s |
dlt |
|
|
Hevo |
|
|
Estuary Flow |
|
|
Debezium change events |
|
|
Kafka Connect |
|
|
AWS DMS |
|
Its control tables ( |
Google Datastream |
|
The |
Snowflake Openflow |
|
|
Databricks Lakeflow Connect |
|
Nothing of its own; its tables come from its pipelines’ configuration (see Loader configuration) |
Matillion Data Loader |
|
|
Rivery (Boomi Data Integration) |
|
|
Polytomic |
|
|
PeerDB |
|
|
Sling |
|
|
Segment-spec event tables (Segment, RudderStack) |
|
|
RudderStack |
|
The Segment-spec tables, in a schema that also holds |
Segment |
|
The Segment-spec tables, on a table the project says Segment lands |
Snowplow |
|
The atomic |
Census |
|
Its |
Hightouch |
|
|
A loader’s bookkeeping tables and reverse ETL logs have loader_role set (metadata, or raw for Airbyte’s raw tables), are fed by its task, and are not counted as landed tables. Any schema where a loader landed data, except Census and Hightouch, which only log what they sent, is evidence for an ontology pack.
Portable, Supermetrics, Windsor.ai, Azure Data Factory and Databricks Lakeflow Connect leave no documented mark on the tables they write (their names and columns are the user’s choice, or the source’s own), so in the warehouse their tables are recognised only when a dbt source names the loader; Lakeflow Connect’s are also read from its pipelines’ configuration.
Loader Configuration
Discovery reads a file only when its name and a look at its text show one of these formats, so other Python, YAML and JSON files cost one read and are left alone; files over 2 MB are not read. Nothing is imported, run or connected to. A file Konstruct cannot read, such as one nested too deeply or with a value of the wrong shape, is a warning, and the build goes on without it. Every YAML file Konstruct reads, these and the dbt projects, profiles, definitions, Ossie files and ontology packs, cannot be read when its aliases would add more than a million values once expanded, or when it holds a value YAML cannot give, such as a date that does not exist, an integer of more than 4,300 digits or an escaped lone surrogate; a dbt project, profile, definitions, Ossie or pack file cannot be read either when it is neither UTF-8 nor UTF-16. A table name filled in when the loader runs ($VAR, ${VAR}, a Terraform expression) is a warning too, and that table’s lineage is left out rather than read as a name.
File |
Loader |
What Konstruct reads |
|---|---|---|
dlt pipeline ( |
|
Each |
dlt schema ( |
|
Each table and the resource it comes from; nested tables and dlt’s own tables are left out, and the tables are found by name, since the dataset is not in the file |
|
|
Each extractor paired with the loader that follows it in a job’s |
Singer catalog ( |
|
Each selected stream, its source schema ( |
Singer state ( |
|
The streams it holds bookmarks for |
Airbyte Octavia project ( |
|
Each selected stream with its namespace and fields (the selected fields, or every property of its schema), the source connector ( |
Airbyte connection exported from its API ( |
|
The same, from the configuration API’s |
Terraform ( |
|
|
Kafka Connect connector ( |
|
A Debezium connector (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, Db2, MongoDB): each table its |
Estuary Flow catalog ( |
|
Each capture’s bindings ( |
Sling replication (YAML with |
|
Each stream ( |
Databricks Asset Bundle with Lakeflow Connect pipelines (YAML with |
|
Each pipeline’s |
AWS DMS table mappings ( |
|
Each table a selection rule includes by name and no rule excludes, landed under the names its transformation rules give ( |
A file’s tables refine what the warehouse shows: Singer columns on a table meltano.yml lands make it Meltano’s. When the warehouse already has the loader’s sync task for a landed table, the file’s source tables feed that task, which records the file in path and the pipeline in pipeline; otherwise the task is the pipeline’s own (task.dlt.shop). Each edge from a file carries it as evidence (pipelines/shop.py:4). A file listed in [ingestion] paths adds a table the warehouse does not have, or every table when there is no warehouse, as an inferred table. A file that discovery found reaches only the tables the warehouse, dbt or SQL already show, since an application’s own file can look like a loader’s; list a file in [ingestion] paths to add the tables it lands. A column the file names is linked to the source system’s column of the same name when the landed table has it, or when Konstruct knows no columns of the landed table.
Several Repositories
repositories in konstruct.toml lists other repositories to build into the same graph, such as a marketing dbt project that reads tables a finance repository builds:
repositories = ["../marketing", "../platform/airflow"]
Each repository is built with its own configuration (its own konstruct.toml, if it has one), under the options of the build that lists it: --no-dbt, --no-warehouse and --dialect apply to every repository. Its analyses are cached under konstruct-out/repositories/<name>/ of the repository that lists it; the name is the folder’s, with its parent’s added when two folders share one (b-analytics). Konstruct writes nothing to the other repositories, though dbt writes its usual target/ there when it runs. The repositories a listed repository lists in turn are not followed.
A relation two repositories share is one node, and a model one repository builds that another reads as a dbt source is linked column by column, so impact and trace cross from one repository to the next. A business term in one repository’s definitions.yml can name a column another repository builds, so the finance and marketing repositories share one definition of revenue; a term defined in more than one repository is a build warning. Each repository’s SQL is analysed before the graphs are merged, so a SELECT * over a relation another repository builds is not expanded (a build warning says so); columns the SQL names are linked. Two different things with the same id, such as two dbt packages both called shop with a model stg in different schemas, stay apart: the later repository’s keeps its id with @<name> added (model.shop.stg@marketing). Nodes from another repository carry its name in repository and their paths relative to this one, the build watches their files for --update and konstruct status, konstruct status lists what it finds in each repository, and their warnings are prefixed with the repository’s name.
How Lineage Works
Konstruct qualifies each statement against the columns it knows (from the dbt catalog, the warehouse, or the lineage of upstream models, processed in dependency order), so
SELECT *expands. A relation whose columns only queries that read it, or its YAML, name is not taken to have only those. sqlglot then follows every output column through CTEs, subqueries, aliases, unions and window functions to physical columns.A path made only of column references and renames is
direct; anything else (arithmetic, functions, aggregates,CASE, window functions) istransformed.Columns that change which rows a model returns, without being selected (join keys, filters, GROUP BY, HAVING, QUALIFY, DISTINCT and DISTINCT ON, the columns a UNION without ALL, an INTERSECT or an EXCEPT compares, and the ORDER BY of a LIMIT), are recorded as
feedsedges withrole: reference, traced through CTEs. Impact reports the relations that use them but does not then mark every downstream column, which would bury the answer.Nested data is followed:
o.customer.address.city,customer['country'],items[1].sku,unnest(items)andFROM t, UNNEST(t.items) AS iwithi.skutrace to the nested column (orders.items.sku), in outputs and in conditions, through CTEs; reading a field or unnesting a list counts as a direct pass-through. Struct columns, and the structs inside lists and map values, are listed field by field for every engine, named the way SQL reads them.Without compiled SQL, dbt models are rendered by Konstruct in a sandbox: loops, variables,
var()with the valuesvarsindbt_project.ymlgives the project’s models (the global ones, with the block under the project’s name over them; a package’s macros called from a model see the same),ref,source,config, the project’s and packages’ macros (dbt_utils.*),adapter.dispatchand dbt’s cross-database macros (dbt.date_trunc,dbt.cast,dbt.concat,dbt.hash,dbt.date_spineand others). An unknown macro becomes a function call that keeps its arguments, so the columns passed to it stay in the lineage.var()reads only the project’sdbt_project.yml: a package’s owndbt_project.ymlvars and values passed withdbt --varsare not seen, and a value that is itself a template ("{{ env_var('START_DATE') }}") is unknown, so the call’s default is used. Build the project with dbt, so its compiled SQL is read, where those values change the lineage.dbt models, warehouse tables with the same relation name, Airflow SQL targets and Iceberg tables merge into one node, so lineage crosses tools: a column computed in dbt, copied into a reporting table by an Airflow task, and used by a dashboard is one chain.