feat(query): add history-based data lineage - #20235
Draft
youngsofun wants to merge 5 commits into
Draft
Conversation
youngsofun
force-pushed
the
codex/lineage-pipeline-get-lineage
branch
5 times, most recently
from
August 1, 2026 14:43
97af851 to
b41e66b
Compare
youngsofun
force-pushed
the
codex/lineage-pipeline-get-lineage
branch
from
August 2, 2026 01:37
b41e66b to
1ef8574
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I hereby agree to the terms of the CLA available at: https://docs.databend.com/dev/policies/cla/
Summary
Add history-based object and column lineage for successful Databend queries, and expose bounded upstream/downstream traversal through
GET_LINEAGE.Query execution remains authoritative: lineage extraction, logging, and asynchronous history aggregation are best effort and never change the result of the DDL or DML that produced the data.
Concepts and contract
Every lineage edge has one fixed data-flow orientation:
Traversal direction never reverses the meaning or output order of source and target.
The SQL interface is:
GET_LINEAGE( '<object_name>', 'TABLE' | 'VIEW' | 'STAGE' | 'COLUMN', 'UPSTREAM' | 'DOWNSTREAM' [, <distance> ] )distancedefaults to 5 and must be between 1 and 5. COLUMN input usestable.column, with normal catalog/database qualification and quoted identifiers supported.The result contains:
Only active, visible edges are returned. For COLUMN results,
target_statusisMASKEDwhen the resolved target column currently has a masking policy; otherwise it isACTIVE. Object-level results remainACTIVE. The status always describes the target side of the returnedsource -> targetedge, independent of whether the query walks UPSTREAM or DOWNSTREAM.processis JSON containingquery_id,query_kind,lineage_kind, andevent_time.Architecture
Planner extraction
The resolver starts at each target value expression and follows optimizer symbol definitions back to base relation columns. It records only columns that contribute to target values; filter-only, join-only, ordering-only, and
WHEN-only columns are excluded unless they also contribute to a written value.count(col)recordscol, whilecount(*)has no column source.The canonical in-memory model is:
Relations carry catalog, database, name, optional stable ID, catalog type, and relation kind (
TABLE,VIEW, orSTAGE). Columns carry both name and column ID; persistence selects the stable representation appropriate for the endpoint.View and Stream semantics are deliberately different:
Successful-query logging
Extraction runs only when
lineage_unresolvedhistory is enabled. Extraction failures are logged as warnings and do not fail the query. Captured lineage is emitted only after successful pipeline completion or successful empty-pipeline DDL completion.Each event records source/target identities, query/process fields, and two normalized column maps:
Column pairs are sorted and deduplicated before a stable SHA-256 is calculated. Stage edges intentionally carry empty column maps because stage file fields are not a stable schema. Self-loops are removed after endpoint addressing is known.
History aggregation
lineage_unresolvedis merged by:Repeated events with the same physical pattern update the latest query/process time. Different column mappings remain separate rows. CTAS, CREATE VIEW, and DML remain distinct because
lineage_kindparticipates in the identity.Lineage has no retention by default. If retention is configured, only old DML patterns expire; CTAS and CREATE VIEW records remain. ID-addressed deletion tombstones asynchronously remove edges for permanently deleted objects.
Addressing rules
Default-catalog keys use forms such as
TABLE::ID::42; name-addressed keys useTABLE::NAME::catalog.database.table; Stage keys useSTAGE::NAME::stage_name.GET_LINEAGE execution
GET_LINEAGEis an opaque table function backed by oneAsyncSource.lineage_unresolvedtable instance so all levels read one Fuse snapshot.target_lineage_key; for DOWNSTREAM, filtersource_lineage_key.target_statusfrom the target column's current masking-policy metadata.Default-catalog ID resolution follows
TableIdToName -> database ID -> current tableand verifies the loaded table ID before accepting it. Name-addressed objects must still resolve with the expected TABLE/VIEW type. Stages must currently exist and be visible.External catalog endpoints are name-addressed and terminal in v1. They can be returned as one end of an edge, but traversal does not continue through catalog-specific internal identities.
Each frontier batch is read through a child
QueryContext, local pipeline, andPipelinePullingExecutor. The pulling stream runs on an isolated two-thread runtime to avoid starving the outer async source. This follows the existing Recursive CTE nested-pipeline precedent, but the scheduler, cancellation, profiling, runtime creation, and batch materialization behavior deserve focused review.Lifecycle behavior
Supported statements and expressions
The implementation covers CREATE VIEW, CTAS, INSERT SELECT, multi-table INSERT, REPLACE, UPDATE/MERGE mutation paths, COPY to/from named Stage,
INSERT FROM @stage, andINSERT SELECT FROM @stage.Planner coverage includes aliases, casts, scalar expressions, aggregate arguments, subqueries, UNION ALL, ordinary CTEs, automatically materialized CTEs, View boundaries, Stream pass-through, and self-insert suppression.
Column lineage also reports whether each resolved target column is currently protected by a masking policy. Masking is a live status annotation: it does not hide the edge, change traversal, or mark the containing table as masked.
Known limitations and review focus
AS MATERIALIZEDCTE producer-column expansion is not supported.system_historyrequires the SystemHistory enterprise feature. Configuringlineage_unresolvedas invisible currently also preventsGET_LINEAGEfrom reading it.Tests
Unit coverage includes planner extraction, View/Stream semantics, CTEs, aggregates, subqueries, Stage sources, catalog/engine addressing, normalized bidirectional column maps, hash identity, self-loop filtering, history MERGE replay behavior, tombstones, retention, argument parsing, target masking status, and traversal helpers.
History sqllogic coverage includes object and column traversal in both directions, multi-hop and multiple mapping patterns, column-level
MASKEDstatus without changing object-levelACTIVEstatus, View boundaries, Stream pass-through, CTAS/DML/COPY/multi-insert/replace, Stage endpoints, self-insert, rename/drop/undrop/replace/vacuum behavior, column replacement, and an Iceberg REST catalog fixture. History readiness is polled rather than relying on one fixed sleep.Static validation before publishing this PR:
cargo fmt --all -- --checkgit diff --check up/main...HEADbash -nfor the modified history test scriptsType of change
AI assistance
This change is