AI-Powered Data Lineage Tracking
Imagine changing a table structure in the staging layer and the next day sales dashboards show zeros. The cause: a missed reference in an SQL transformation. Without data lineage, finding it takes hours of manual digging. When pipelines number in the dozens and dashboards in the hundreds, manual impact analysis becomes a bottleneck. We automate building the graph that shows every source and transformation, delivering impact analysis in seconds.
We integrate AI-powered data lineage tracking that automatically builds a graph from your SQL, dbt models, and Python code. Without it, locating the source of a column error is hours of manual search. Our solution builds the graph on the fly, without manual documentation, covering 80-90% of typical ETL patterns. The remaining 10% is handled via runtime tracking with OpenLineage.
Why Automating Data Lineage Is Critical
Lost lineage is a common cause of downtime in data platforms. Changing one column in the raw layer can break 20+ BI dashboards. Without automatic tracking, impact analysis takes days, and errors are discovered post-factum. AI tracking gives you:
- dependency graph — updated after every deploy
- impact analysis — in seconds: "what breaks if I delete table X"
- transformation audit — where data came from and how it changed
Manual impact analysis takes 2-3 days; AI tracking takes less than 100 ms on a graph of 1000 nodes. The difference is thousands-fold. Data support budget savings reach 40%, and audit time drops from 2 days to 2 hours.
What Impact Analysis Delivers
Impact analysis answers "what breaks if I change this table." Without it, every change is a risk. AI tracking gives you a full list of affected dashboards, models, and pipelines in seconds. For example, deleting the client_id column in the staging layer might affect 15 dashboards, 3 dbt models, and 2 API endpoints. Impact analysis shows this before deployment.
How We Build the Data Lineage Graph
Parsing SQL and dbt Models
For extracting lineage from SQL, we use sqlglot for parsing and LLMs (Claude 3.5, GPT-4o) for complex cases — dynamic SQL, window functions, ORM. For dbt, we read the manifest.json and build a graph based on ref dependencies.
from anthropic import Anthropic
import sqlparse
import sqlglot
import networkx as nx
import json
from dataclasses import dataclass
@dataclass
class LineageNode:
node_id: str
name: str
node_type: str # table, view, query, model, api, file
schema: dict = None
metadata: dict = None
@dataclass
class LineageEdge:
source: str
target: str
transform_type: str # select, join, aggregation, filter, union
columns_mapped: dict = None # {source_col: target_col}
class DataLineageTracker:
def __init__(self):
self.llm = Anthropic()
self.graph = nx.DiGraph()
self.nodes = {}
def parse_sql_lineage(self, sql: str, output_table: str = None) -> dict:
"""Extract lineage from an SQL query"""
try:
# Parse via sqlglot
statements = sqlglot.parse(sql)
lineage = {'sources': [], 'targets': [], 'columns': {}}
for stmt in statements:
# Tables in FROM and JOIN
for table in stmt.find_all(sqlglot.expressions.Table):
if table.name:
lineage['sources'].append(table.name)
# Target table (CREATE TABLE AS / INSERT INTO)
if isinstance(stmt, sqlglot.expressions.Create):
lineage['targets'].append(str(stmt.this))
elif isinstance(stmt, sqlglot.expressions.Insert):
lineage['targets'].append(str(stmt.this))
if output_table:
lineage['targets'].append(output_table)
# Column mapping via LLM for complex cases
lineage['column_mapping'] = self._extract_column_mapping(sql)
return lineage
except Exception:
# Fallback: LLM parsing
return self._llm_parse_lineage(sql, output_table)
def _llm_parse_lineage(self, sql: str, output_table: str = None) -> dict:
"""LLM extraction of lineage for complex SQL"""
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=400,
messages=[{
"role": "user",
"content": f"""Extract data lineage from this SQL.
SQL:
{sql[:1500]}
Output table: {output_table or "unknown"}
Return JSON:
{{
"sources": ["table1", "table2"],
"targets": ["output_table"],
"transforms": ["aggregation", "join"],
"column_mapping": {{"source.col1": "target.col_a"}}
}}"""
}]
)
try:
return json.loads(response.content[0].text)
except Exception:
return {'sources': [], 'targets': [], 'transforms': [], 'column_mapping': {}}
def _extract_column_mapping(self, sql: str) -> dict:
"""Column mapping source → target"""
try:
parsed = sqlglot.parse_one(sql)
mapping = {}
for col in parsed.find_all(sqlglot.expressions.Column):
alias = col.find_ancestor(sqlglot.expressions.Alias)
if alias:
target_name = str(alias.alias)
source_name = str(col)
mapping[source_name] = target_name
return mapping
except Exception:
return {}
def add_dbt_lineage(self, manifest_path: str):
"""Import lineage from dbt manifest.json"""
with open(manifest_path) as f:
manifest = json.load(f)
for node_id, node in manifest.get('nodes', {}).items():
if node.get('resource_type') == 'model':
model_name = node['name']
# Add model node
self.graph.add_node(model_name, **{
'type': 'dbt_model',
'schema': node.get('database', '') + '.' + node.get('schema', ''),
'description': node.get('description', ''),
'tags': node.get('tags', [])
})
# Dependencies (upstream)
for dep in node.get('depends_on', {}).get('nodes', []):
dep_name = dep.split('.')[-1]
self.graph.add_edge(dep_name, model_name,
transform_type='dbt_ref')
def build_lineage_from_code(self, python_code: str,
file_name: str = "transform.py") -> dict:
"""Extract lineage from Python transformation code"""
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=400,
messages=[{
"role": "user",
"content": f"""Extract data lineage from this Python ETL code.
File: {file_name}
Code:
{python_code[:1500]}
Return JSON:
{{
"reads_from": ["table/file/api names"],
"writes_to": ["output table/file names"],
"transforms": ["description of transformations applied"],
"column_transforms": ["human-readable descriptions of column transformations"]
}}"""
}]
)
try:
return json.loads(response.content[0].text)
except Exception:
return {}
Lineage Graph and Impact Analysis
def get_downstream_impact(self, source_table: str) -> dict:
"""What breaks if source_table is changed"""
if source_table not in self.graph:
return {'affected': [], 'count': 0}
# BFS to get all downstream nodes
affected = []
visited = set()
queue = [source_table]
while queue:
current = queue.pop(0)
if current in visited:
continue
visited.add(current)
successors = list(self.graph.successors(current))
for succ in successors:
if succ != source_table:
affected.append({
'node': succ,
'distance': nx.shortest_path_length(self.graph, source_table, succ),
'type': self.graph.nodes[succ].get('type', 'unknown')
})
queue.extend(successors)
affected.sort(key=lambda x: x['distance'])
return {
'source': source_table,
'affected': affected,
'count': len(affected)
}
def get_upstream_sources(self, target_table: str) -> dict:
"""Where data in target_table comes from"""
if target_table not in self.graph:
return {'sources': [], 'path': []}
# All ancestors
ancestors = list(nx.ancestors(self.graph, target_table))
paths = {}
for source in ancestors:
try:
path = nx.shortest_path(self.graph, source, target_table)
paths[source] = path
except nx.NetworkXNoPath:
pass
# AI explanation of lineage
explanation = self._explain_lineage(target_table, ancestors, paths)
return {
'target': target_table,
'sources': ancestors,
'paths': paths,
'explanation': explanation
}
def _explain_lineage(self, target: str, sources: list, paths: dict) -> str:
"""LLM explanation of data lineage"""
paths_summary = json.dumps(
{s: p for s, p in list(paths.items())[:5]},
ensure_ascii=False
)
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=300,
messages=[{
"role": "user",
"content": f"""Explain the data lineage for table "{target}".
Source tables: {sources}
Key paths: {paths_summary}
Summarize: where data originates, what transformations occur, potential data quality risks.
2-4 sentences, non-technical language."""
}]
)
return response.content[0].text
def detect_lineage_breaks(self) -> list[dict]:
"""Detect breaks in lineage"""
breaks = []
# Tables without sources (except raw)
for node in self.graph.nodes():
if self.graph.in_degree(node) == 0:
node_data = self.graph.nodes[node]
if node_data.get('type') not in ['raw_table', 'external_source']:
breaks.append({
'node': node,
'issue': 'no_upstream_lineage',
'severity': 'warning'
})
# Tables without consumers
if self.graph.out_degree(node) == 0:
breaks.append({
'node': node,
'issue': 'orphaned_dataset',
'severity': 'info'
})
return breaks
Lineage tracking via SQL parsing + LLM augmentation covers 80-90% of typical ETL patterns. For complex tricky transformations (dynamic SQL, ORM), runtime tracing is needed. OpenLineage + Marquez is the standard for automatic lineage collection from Airflow, Spark, and dbt without writing code. We integrate OpenLineage into your existing pipeline in 1-2 days, adding transparency without code changes.
Step-by-Step Implementation of AI Tracking
- Audit — collect SQL, dbt, Python code of all pipelines.
- Parsing — sqlglot + LLM extract dependencies and column mappings.
- Graph building — NetworkX creates a DAG with nodes and edges.
- Impact analysis — implement API for downstream/upstream queries.
- CI/CD integration — automatic graph update on every deploy.
- Testing — verify coverage and accuracy on real data.
Case: Retail Dashboard Lineage Automation
A large retailer had 50+ dashboards in Mode Analytics built on 30 dbt models. After changing the staging layer structure (adding a discount column), one dashboard started showing incorrect totals. Without lineage, finding the cause took 3 days. After implementing AI tracking, impact analysis showed 5 dashboards and 2 dbt models affected. Solution: automatic graph update on every commit. Error search time dropped to 10 minutes.
What You Get
- Auto-updating data lineage graph (dashboard based on NetworkX + D3.js).
- Impact analysis API with upstream/downstream queries.
- Detection of lineage breaks (tables without sources or consumers).
- CI/CD integration (GitHub Actions / GitLab CI) for auto-updates.
- Architecture documentation and team instructions.
- Team training (2-hour workshop).
- 3 months post-implementation support.
Implementation Phases
| Phase | Content | Duration (days) |
|---|---|---|
| Current pipeline audit | Collect SQL, dbt, Python code; identify gaps | 1-2 |
| Graph building | Parsing, LLM augmentation, manual testing | 3-7 |
| Impact analysis | API, dashboard, break detection | 2-3 |
| CI/CD integration | Auto-update on deploy | 1-2 |
| Documentation and training | README, dashboards, team instructions | 1-2 |
Comparison: Manual vs AI Tracking
| Criterion | Manual Tracking | AI Tracking |
|---|---|---|
| Time per pipeline | 2-3 weeks | from 5 days |
| Automation coverage | 0% | 80-90% |
| Impact analysis latency (1000 nodes) | hours | < 100 ms |
| Update on deploy | manual | automatic |
AI tracking is 50x faster than manual impact analysis, and data support savings exceed 40%. We'll evaluate your project — just reach out. Our experience: 5+ years in MLOps, over 20 implemented data lineage projects. Certified in OpenLineage. Get a consultation — contact us.







