feat(query): add history-based data lineage - #20230
Closed
youngsofun wants to merge 7 commits into
Closed
Conversation
12 tasks
Contributor
🤖 CI Job Analysis
📊 Summary
❌ NO RETRY NEEDEDAll failures appear to be code/test issues requiring manual fixes. 🔍 Job Details
🤖 AboutAutomated analysis using job annotations to distinguish infrastructure issues (auto-retried) from code/test issues (manual fixes needed). |
youngsofun
force-pushed
the
codex/lineage-history
branch
from
July 30, 2026 09:53
48a8d8e to
5b81988
Compare
12 tasks
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, with bounded graph traversal through
GET_LINEAGE.The feature has three parts:
system_history.GET_LINEAGE.This keeps lineage outside table-meta transactions and query commit retries. A lineage logging or aggregation failure does not change the result of the DDL or DML that produced the data.
This PR is stacked on #20201, which contains the first two commits for
system.columns.column_idand planner lineage extraction. After #20201 merges, this branch will be rebased and those shared commits will disappear from this diff.Concepts and terminology
Lineage describes how data moves or is derived across objects and columns.
Every persisted edge has one fixed data-flow orientation:
sourceandtargettherefore describe the edge itself.UPSTREAMandDOWNSTREAMdescribe how a user traverses the graph; they do not reverse the stored edge meaning.The implementation records two related domains:
table_a -> table_borstage -> table.table_a.a -> table_b.x. Object lineage remains available even when a source cannot provide a stable column mapping.Query contract
The user-facing goal is a Snowflake-compatible traversal shape:
GET_LINEAGE( '<object_name>', '<object_domain>', '<direction>' [, <distance> ] )object_domain:TABLE,VIEW,STAGE, orCOLUMN.direction:UPSTREAMorDOWNSTREAM.distance: an integer from 1 to 5; the default is 5.catalog.database.table.column.The result contains:
target_statusis currentlyACTIVE.processis JSON containingquery_id,query_kind,lineage_kind, andevent_time.GET_LINEAGEis the stable SQL interface for interactive traversal. A Cloud UI or graph service that needs to load many edges at once may query the resolvedsystem_history.lineageview directly. The directionallineage_by_sourceandlineage_by_targetviews are optimized traversal inputs, whilelineage_unresolvedis the ingestion table containing captured identities.Architecture
1. Planner extraction
The planner builds symbol definitions from the optimized
SExpr, then starts from each target value expression and recursively resolves referenced symbols to base relation columns.The public model is storage-independent:
QueryLineageRelationcarries catalog, database, name, optional stable ID, catalog type, and relation kind (TABLE,VIEW, orSTAGE).QueryLineageColumncarries both name and column ID.Only expressions that produce target values are traversal roots. Filter-only, join-only,
WHEN-only, and ordering-only columns are excluded unless they also contribute to a target value. For example,count(col)recordscol, whilecount(*)has no source column edge.2. Successful-query logging
When lineage history is enabled, the interpreter attaches extracted lineage to
QueryContext. DML logs it only after the pipeline reports success. DDL paths log only after the object operation succeeds. Logging is best effort and never changes the SQL result.Each
LineageLogEntryrecords:source_to_target_columnsandtarget_to_source_columnsmaps.Both column-map directions are stored because column traversal must efficiently select only columns reachable from the current frontier.
Resolved source and target endpoints are compared before logging, so an object writing to itself does not persist a self-loop. Other real sources in the same statement are preserved.
3. History aggregation
GlobalHistoryLogloads structured logs through the existinglog_historybatch pipeline. The lineage transform is replay-safe and merges by:Within one batch, duplicate identities select the newest event before
MERGE. A matching row updates its latest query/process time; a different column mapping has a different hash and remains as another physical pattern. CTAS, CREATE VIEW, and DML remain distinct becauselineage_kindparticipates in the identity.The raw table stores forward and reverse column maps rather than one row per column pair. This avoids repeating all object/process fields for wide relations while still allowing
map_pick-style traversal.Lineage has no retention by default. Users can configure retention for
lineage_unresolved; its delete template only expires DML records. Structural CREATE VIEW and CTAS records remain permanent. Object-deletion tombstones asynchronously remove ID-addressed edges that have become permanently invalid.4. Active-object resolution
system_history.lineageresolves captured endpoints against current metadata:Two directional views retain the fixed source/target columns but choose different frontier keys:
lineage_by_targetsupportsUPSTREAMtraversal.lineage_by_sourcesupportsDOWNSTREAMtraversal.5. Bounded traversal
The binder expands
GET_LINEAGEinto one SQL query with CTE generations from level 1 through the requested distance. Each generation joins the previous frontier to the appropriate directional view, resolves the next object or column, and removes duplicate semantic edges before the next level.The final result prefers the shortest distance. If the same semantic result is represented by multiple physical history patterns, it uses the newest process information deterministically.
Supported lineage
Statements
CREATE VIEWINSERT ... SELECTREPLACE INTO ... SELECTUPDATE ... FROMMERGECOPY INTOfrom/to named Stage endpointsThe planner also follows aliases, casts, scalar expressions, aggregate/window function arguments, scalar subqueries,
UNION ALL, CTEs, automatically materialized CTEs, UDFs, async functions, and set-returning expressions.Object and engine handling
Lifecycle semantics
Known limitations
GET_LINEAGEinitialization from an external-catalog object is not supported in v1 because the start-object lookup uses Databend system metadata. External objects can still appear as returned endpoints.AS MATERIALIZEDCTE lineage is not supported because the query-scoped temporary table loses its original producer-column mapping. Automatically materialized CTEs are supported.target_statuscurrently only reportsACTIVE.query_id, query kind, lineage kind, and event time, but not query text, stored-procedure context, task name, or full process metadata. A UI can currently joinquery_idto query history when those details are needed. If lineage must become independent of query-history retention, those fields should be added to the lineage log/history schema.Tests
Unit and static SQL tests
MERGEidentity, batch deduplication, delete tombstones, and optional retention.GET_LINEAGESQL shape for both directions and all distances.system.columns.column_idschema coverage.End-to-end history tests
tests/logging/test-history-tables.shstarts a two-node history-enabled query deployment, prepares each scenario through sqllogictest, polls the history views until the expected batches are visible, then runs the matching check suite. It does not rely on one fixed sleep for lineage readiness.The suites cover:
Local validation:
cargo fmt --all -- --checkgit diff --checkbash -n tests/logging/test-history-tables.shcargo test -p databend-common-tracing predefined_tables::history_tables::tests --libcargo check -p databend-common-configcargo check -p databend-query --libType of change
AI assistance
This change is