@duckcodeailabs/dql-cli 0.8.9 → 0.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/dql-notebook/assets/index-Cxj__xjY.js +564 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/lineage.d.ts +4 -4
- package/dist/commands/lineage.d.ts.map +1 -1
- package/dist/commands/lineage.js +126 -246
- package/dist/commands/lineage.js.map +1 -1
- package/dist/local-runtime.d.ts +24 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +387 -146
- package/dist/local-runtime.js.map +1 -1
- package/dist/local-runtime.test.js +130 -1
- package/dist/local-runtime.test.js.map +1 -1
- package/dist/semantic-import.js +11 -11
- package/dist/semantic-import.js.map +1 -1
- package/dist/semantic-import.test.js +89 -272
- package/dist/semantic-import.test.js.map +1 -1
- package/package.json +9 -9
- package/dist/assets/dql-notebook/assets/index-Cp34wXvX.js +0 -558
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-Cxj__xjY.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/react-CRB3T2We.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-CCrEt63p.js">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/index-BZV40eAE.css">
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* Usage:
|
|
8
8
|
* dql lineage [path] Show full lineage graph summary
|
|
9
9
|
* dql lineage <name> [path] Show upstream/downstream for a block, table, or metric
|
|
10
|
-
* dql lineage --search <term> Search all nodes by name
|
|
11
|
-
* dql lineage --focus <name> [--depth N] Focused subgraph centered on a node
|
|
12
|
-
* dql lineage --dashboard <name> Show what feeds a dashboard/notebook
|
|
13
|
-
* dql lineage --dbt Show the dbt model DAG portion
|
|
14
10
|
* dql lineage --table <name> [path] Show lineage for a specific source table
|
|
15
11
|
* dql lineage --metric <name> [path] Show lineage for a specific metric
|
|
16
12
|
* dql lineage --domain <name> [path] Show lineage within a domain
|
|
17
13
|
* dql lineage --impact <name> [path] Impact analysis: what breaks if this node changes?
|
|
18
14
|
* dql lineage --trust-chain <from> <to> Show trust chain between two blocks
|
|
15
|
+
* dql lineage --search <term> [path] Search lineage nodes by name
|
|
16
|
+
* dql lineage --focus <name> [path] Show a focused lineage subgraph
|
|
17
|
+
* dql lineage --dashboard <name> [path] Show lineage for a dashboard/notebook
|
|
18
|
+
* dql lineage --dbt [path] Show the dbt portion of lineage
|
|
19
19
|
* dql lineage --export [path] Export lineage as JSON
|
|
20
20
|
* dql lineage --no-manifest Force live scan (skip dql-manifest.json)
|
|
21
21
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../src/commands/lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAoBH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAsB,UAAU,CAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../src/commands/lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAoBH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAsB,UAAU,CAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,CAiHf"}
|
package/dist/commands/lineage.js
CHANGED
|
@@ -7,21 +7,21 @@
|
|
|
7
7
|
* Usage:
|
|
8
8
|
* dql lineage [path] Show full lineage graph summary
|
|
9
9
|
* dql lineage <name> [path] Show upstream/downstream for a block, table, or metric
|
|
10
|
-
* dql lineage --search <term> Search all nodes by name
|
|
11
|
-
* dql lineage --focus <name> [--depth N] Focused subgraph centered on a node
|
|
12
|
-
* dql lineage --dashboard <name> Show what feeds a dashboard/notebook
|
|
13
|
-
* dql lineage --dbt Show the dbt model DAG portion
|
|
14
10
|
* dql lineage --table <name> [path] Show lineage for a specific source table
|
|
15
11
|
* dql lineage --metric <name> [path] Show lineage for a specific metric
|
|
16
12
|
* dql lineage --domain <name> [path] Show lineage within a domain
|
|
17
13
|
* dql lineage --impact <name> [path] Impact analysis: what breaks if this node changes?
|
|
18
14
|
* dql lineage --trust-chain <from> <to> Show trust chain between two blocks
|
|
15
|
+
* dql lineage --search <term> [path] Search lineage nodes by name
|
|
16
|
+
* dql lineage --focus <name> [path] Show a focused lineage subgraph
|
|
17
|
+
* dql lineage --dashboard <name> [path] Show lineage for a dashboard/notebook
|
|
18
|
+
* dql lineage --dbt [path] Show the dbt portion of lineage
|
|
19
19
|
* dql lineage --export [path] Export lineage as JSON
|
|
20
20
|
* dql lineage --no-manifest Force live scan (skip dql-manifest.json)
|
|
21
21
|
*/
|
|
22
22
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
23
23
|
import { join, extname, resolve } from 'node:path';
|
|
24
|
-
import { Parser, loadSemanticLayerFromDir, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview,
|
|
24
|
+
import { Parser, loadSemanticLayerFromDir, buildLineageGraph, buildManifest, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, LineageGraph, queryLineage, } from '@duckcodeailabs/dql-core';
|
|
25
25
|
export async function runLineage(blockNameOrPath, rest, flags) {
|
|
26
26
|
// Collect all args and separate flags from positional args
|
|
27
27
|
const allArgs = [...(blockNameOrPath ? [blockNameOrPath] : []), ...rest];
|
|
@@ -55,28 +55,6 @@ export async function runLineage(blockNameOrPath, rest, flags) {
|
|
|
55
55
|
console.log(JSON.stringify(graph.toJSON(), null, 2));
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
// --search <term>
|
|
59
|
-
const searchIdx = allArgs.indexOf('--search');
|
|
60
|
-
if (searchIdx >= 0 && allArgs[searchIdx + 1]) {
|
|
61
|
-
return printSearch(graph, allArgs[searchIdx + 1]);
|
|
62
|
-
}
|
|
63
|
-
// --focus <name> [--depth N]
|
|
64
|
-
const focusIdx = allArgs.indexOf('--focus');
|
|
65
|
-
if (focusIdx >= 0 && allArgs[focusIdx + 1]) {
|
|
66
|
-
const depthIdx = allArgs.indexOf('--depth');
|
|
67
|
-
const depth = depthIdx >= 0 && allArgs[depthIdx + 1] ? Number(allArgs[depthIdx + 1]) : undefined;
|
|
68
|
-
return printFocused(graph, allArgs[focusIdx + 1], depth);
|
|
69
|
-
}
|
|
70
|
-
// --dashboard <name>
|
|
71
|
-
const dashIdx = allArgs.indexOf('--dashboard');
|
|
72
|
-
if (dashIdx >= 0 && allArgs[dashIdx + 1]) {
|
|
73
|
-
const nodeId = resolveNodeId(graph, allArgs[dashIdx + 1]) ?? `dashboard:${allArgs[dashIdx + 1]}`;
|
|
74
|
-
return printNodeLineage(graph, nodeId, flags);
|
|
75
|
-
}
|
|
76
|
-
// --dbt (show dbt model DAG)
|
|
77
|
-
if (allArgs.includes('--dbt')) {
|
|
78
|
-
return printDbtDAG(graph);
|
|
79
|
-
}
|
|
80
58
|
// --impact <name>
|
|
81
59
|
const impactIdx = allArgs.indexOf('--impact');
|
|
82
60
|
if (impactIdx >= 0 && allArgs[impactIdx + 1]) {
|
|
@@ -93,6 +71,23 @@ export async function runLineage(blockNameOrPath, rest, flags) {
|
|
|
93
71
|
if (trustIdx >= 0 && allArgs[trustIdx + 1] && allArgs[trustIdx + 2]) {
|
|
94
72
|
return printTrustChain(graph, allArgs[trustIdx + 1], allArgs[trustIdx + 2], flags);
|
|
95
73
|
}
|
|
74
|
+
const searchIdx = allArgs.indexOf('--search');
|
|
75
|
+
if (searchIdx >= 0 && allArgs[searchIdx + 1]) {
|
|
76
|
+
return printSearchResults(graph, allArgs[searchIdx + 1]);
|
|
77
|
+
}
|
|
78
|
+
const focusIdx = allArgs.indexOf('--focus');
|
|
79
|
+
if (focusIdx >= 0 && allArgs[focusIdx + 1]) {
|
|
80
|
+
return printFocusedLineage(graph, allArgs[focusIdx + 1]);
|
|
81
|
+
}
|
|
82
|
+
const dashboardIdx = allArgs.indexOf('--dashboard');
|
|
83
|
+
if (dashboardIdx >= 0 && allArgs[dashboardIdx + 1]) {
|
|
84
|
+
return printFocusedLineage(graph, `dashboard:${allArgs[dashboardIdx + 1]}`);
|
|
85
|
+
}
|
|
86
|
+
if (allArgs.includes('--dbt')) {
|
|
87
|
+
const result = queryLineage(graph, { types: ['dbt_model', 'dbt_source'] });
|
|
88
|
+
console.log(JSON.stringify(result.graph, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
96
91
|
// --domain <name>
|
|
97
92
|
if (flags.domain) {
|
|
98
93
|
return printDomainLineage(graph, flags.domain, flags);
|
|
@@ -142,69 +137,81 @@ function loadFromManifestOrScan(projectRoot) {
|
|
|
142
137
|
}
|
|
143
138
|
/** Discover all blocks and semantic layer definitions and build the lineage graph. */
|
|
144
139
|
function buildProjectLineage(projectRoot) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
140
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot);
|
|
141
|
+
try {
|
|
142
|
+
const manifest = buildManifest({ projectRoot, dbtManifestPath });
|
|
143
|
+
return LineageGraph.fromJSON({
|
|
144
|
+
nodes: manifest.lineage.nodes,
|
|
145
|
+
edges: manifest.lineage.edges,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
const blocks = [];
|
|
150
|
+
const metrics = [];
|
|
151
|
+
const dimensions = [];
|
|
152
|
+
const dirs = ['blocks', 'dashboards', 'workbooks'];
|
|
153
|
+
for (const dir of dirs) {
|
|
154
|
+
const dirPath = join(projectRoot, dir);
|
|
155
|
+
if (!existsSync(dirPath))
|
|
156
156
|
continue;
|
|
157
|
-
const
|
|
157
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
158
|
+
if (!entry.isFile() || extname(entry.name) !== '.dql')
|
|
159
|
+
continue;
|
|
160
|
+
const filePath = join(dirPath, entry.name);
|
|
161
|
+
try {
|
|
162
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
163
|
+
const parser = new Parser(source, `${dir}/${entry.name}`);
|
|
164
|
+
const ast = parser.parse();
|
|
165
|
+
for (const stmt of ast.statements) {
|
|
166
|
+
const block = stmt;
|
|
167
|
+
if (block.kind !== 'BlockDecl')
|
|
168
|
+
continue;
|
|
169
|
+
blocks.push({
|
|
170
|
+
name: block.name,
|
|
171
|
+
sql: block.query?.rawSQL ?? '',
|
|
172
|
+
domain: extractBlockProperty(block, 'domain'),
|
|
173
|
+
owner: extractBlockProperty(block, 'owner'),
|
|
174
|
+
status: extractBlockProperty(block, 'status'),
|
|
175
|
+
blockType: block.blockType,
|
|
176
|
+
metricRef: block.metricRef,
|
|
177
|
+
chartType: extractVisualizationChart(block),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Skip unparseable files
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const semanticDir = join(projectRoot, 'semantic-layer');
|
|
187
|
+
if (existsSync(semanticDir)) {
|
|
158
188
|
try {
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
blockType: block.blockType,
|
|
173
|
-
metricRef: block.metricRef,
|
|
174
|
-
chartType: extractVisualizationChart(block),
|
|
189
|
+
const layer = loadSemanticLayerFromDir(semanticDir);
|
|
190
|
+
for (const metric of layer.listMetrics()) {
|
|
191
|
+
metrics.push({
|
|
192
|
+
name: metric.name,
|
|
193
|
+
table: metric.table,
|
|
194
|
+
domain: metric.domain,
|
|
195
|
+
type: metric.type,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
for (const dim of layer.listDimensions()) {
|
|
199
|
+
dimensions.push({
|
|
200
|
+
name: dim.name,
|
|
201
|
+
table: dim.table,
|
|
175
202
|
});
|
|
176
203
|
}
|
|
177
204
|
}
|
|
178
205
|
catch {
|
|
179
|
-
//
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// Load semantic layer
|
|
184
|
-
const semanticDir = join(projectRoot, 'semantic-layer');
|
|
185
|
-
if (existsSync(semanticDir)) {
|
|
186
|
-
try {
|
|
187
|
-
const layer = loadSemanticLayerFromDir(semanticDir);
|
|
188
|
-
for (const metric of layer.listMetrics()) {
|
|
189
|
-
metrics.push({
|
|
190
|
-
name: metric.name,
|
|
191
|
-
table: metric.table,
|
|
192
|
-
domain: metric.domain,
|
|
193
|
-
type: metric.type,
|
|
194
|
-
});
|
|
206
|
+
// Non-fatal
|
|
195
207
|
}
|
|
196
|
-
for (const dim of layer.listDimensions()) {
|
|
197
|
-
dimensions.push({
|
|
198
|
-
name: dim.name,
|
|
199
|
-
table: dim.table,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
// Non-fatal
|
|
205
208
|
}
|
|
209
|
+
return buildLineageGraph(blocks, metrics, dimensions);
|
|
206
210
|
}
|
|
207
|
-
|
|
211
|
+
}
|
|
212
|
+
function resolveDbtManifestPath(projectRoot) {
|
|
213
|
+
const candidate = join(projectRoot, 'target', 'manifest.json');
|
|
214
|
+
return existsSync(candidate) ? candidate : undefined;
|
|
208
215
|
}
|
|
209
216
|
/** Extract a property value from a block — checks direct AST fields first, then properties array. */
|
|
210
217
|
function extractBlockProperty(block, propName) {
|
|
@@ -239,23 +246,14 @@ function printSummary(graph, _flags) {
|
|
|
239
246
|
const edges = graph.getAllEdges();
|
|
240
247
|
const domains = graph.getDomains();
|
|
241
248
|
const sourceTables = graph.getNodesByType('source_table');
|
|
242
|
-
const dbtModels = graph.getNodesByType('dbt_model');
|
|
243
|
-
const dbtSources = graph.getNodesByType('dbt_source');
|
|
244
249
|
const blocks = graph.getNodesByType('block');
|
|
245
250
|
const metrics = graph.getNodesByType('metric');
|
|
246
251
|
const dimensions = graph.getNodesByType('dimension');
|
|
247
252
|
const charts = graph.getNodesByType('chart');
|
|
248
|
-
const dashboards = graph.getNodesByType('dashboard');
|
|
249
253
|
console.log('\n DQL Lineage Summary');
|
|
250
254
|
console.log(' ' + '='.repeat(50));
|
|
251
255
|
// Overview counts
|
|
252
256
|
console.log(`\n ${nodes.length} nodes, ${edges.length} edges, ${domains.length} domain(s)`);
|
|
253
|
-
if (dbtModels.length > 0 || dbtSources.length > 0) {
|
|
254
|
-
console.log(` dbt: ${dbtSources.length} source(s), ${dbtModels.length} model(s)`);
|
|
255
|
-
}
|
|
256
|
-
if (dashboards.length > 0) {
|
|
257
|
-
console.log(` ${dashboards.length} dashboard(s)`);
|
|
258
|
-
}
|
|
259
257
|
// Source Tables
|
|
260
258
|
if (sourceTables.length > 0) {
|
|
261
259
|
console.log(`\n Source Tables (${sourceTables.length}):`);
|
|
@@ -386,10 +384,7 @@ function printDAGNode(graph, node, depth, printed) {
|
|
|
386
384
|
printed.add(node.id);
|
|
387
385
|
const indent = ' ' + ' '.repeat(depth);
|
|
388
386
|
const prefix = depth === 0 ? '' : '└── ';
|
|
389
|
-
const typeLabel = node.type === 'source_table' ? 'table'
|
|
390
|
-
: node.type === 'dbt_model' ? 'dbt_model'
|
|
391
|
-
: node.type === 'dbt_source' ? 'dbt_source'
|
|
392
|
-
: node.type;
|
|
387
|
+
const typeLabel = node.type === 'source_table' ? 'table' : node.type;
|
|
393
388
|
const badge = node.status === 'certified' ? ' ✓' : '';
|
|
394
389
|
const domain = node.domain ? ` [${node.domain}]` : '';
|
|
395
390
|
console.log(`${indent}${prefix}${typeLabel}:${node.name}${domain}${badge}`);
|
|
@@ -411,7 +406,7 @@ function resolveNodeId(graph, name) {
|
|
|
411
406
|
if (name.includes(':') && graph.getNode(name))
|
|
412
407
|
return name;
|
|
413
408
|
// Try common type prefixes in priority order
|
|
414
|
-
const prefixes = ['block', '
|
|
409
|
+
const prefixes = ['block', 'dashboard', 'dbt_model', 'dbt_source', 'table', 'metric', 'dimension', 'chart', 'domain'];
|
|
415
410
|
for (const prefix of prefixes) {
|
|
416
411
|
const id = `${prefix}:${name}`;
|
|
417
412
|
if (graph.getNode(id))
|
|
@@ -424,6 +419,40 @@ function resolveNodeId(graph, name) {
|
|
|
424
419
|
}
|
|
425
420
|
return null;
|
|
426
421
|
}
|
|
422
|
+
function printSearchResults(graph, term) {
|
|
423
|
+
const result = queryLineage(graph, { search: term });
|
|
424
|
+
console.log(`\n Search: ${term}`);
|
|
425
|
+
console.log(' ' + '='.repeat(50));
|
|
426
|
+
if (!result.matches?.length) {
|
|
427
|
+
console.log(' No lineage nodes matched.\n');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
for (const match of result.matches) {
|
|
431
|
+
const node = match.node;
|
|
432
|
+
console.log(` ${node.id}${node.domain ? ` [${node.domain}]` : ''}`);
|
|
433
|
+
}
|
|
434
|
+
console.log('');
|
|
435
|
+
}
|
|
436
|
+
function printFocusedLineage(graph, focus) {
|
|
437
|
+
const result = queryLineage(graph, { focus });
|
|
438
|
+
if (!result.focalNode) {
|
|
439
|
+
console.error(`"${focus}" not found in lineage graph.`);
|
|
440
|
+
process.exitCode = 1;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
console.log(`\n Focused Lineage: ${result.focalNode.name}`);
|
|
444
|
+
console.log(' ' + '='.repeat(50));
|
|
445
|
+
for (const node of result.graph.nodes.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
446
|
+
console.log(` ${node.id}${node.domain ? ` [${node.domain}]` : ''}`);
|
|
447
|
+
}
|
|
448
|
+
if (result.graph.edges.length > 0) {
|
|
449
|
+
console.log('\n Edges:');
|
|
450
|
+
for (const edge of result.graph.edges) {
|
|
451
|
+
console.log(` ${edge.source} -${edge.type}-> ${edge.target}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
console.log('');
|
|
455
|
+
}
|
|
427
456
|
function printNodeLineage(graph, nodeId, _flags) {
|
|
428
457
|
const node = graph.getNode(nodeId);
|
|
429
458
|
if (!node) {
|
|
@@ -431,17 +460,14 @@ function printNodeLineage(graph, nodeId, _flags) {
|
|
|
431
460
|
process.exitCode = 1;
|
|
432
461
|
return;
|
|
433
462
|
}
|
|
434
|
-
const typeLabel = node.type === 'source_table' ? 'Table'
|
|
435
|
-
: node.type === 'dbt_model' ? 'dbt Model'
|
|
436
|
-
: node.type === 'dbt_source' ? 'dbt Source'
|
|
437
|
-
: node.type.charAt(0).toUpperCase() + node.type.slice(1);
|
|
463
|
+
const typeLabel = node.type === 'source_table' ? 'Table' : node.type.charAt(0).toUpperCase() + node.type.slice(1);
|
|
438
464
|
const ancestors = graph.ancestors(nodeId);
|
|
439
465
|
const descendants = graph.descendants(nodeId);
|
|
440
466
|
console.log(`\n ${typeLabel} Lineage: ${node.name}`);
|
|
441
467
|
console.log(' ' + '='.repeat(50));
|
|
442
468
|
// Metadata
|
|
443
469
|
const meta = [];
|
|
444
|
-
if (
|
|
470
|
+
if (node.type !== 'source_table')
|
|
445
471
|
meta.push(`Type: ${node.type}`);
|
|
446
472
|
if (node.domain)
|
|
447
473
|
meta.push(`Domain: ${node.domain}`);
|
|
@@ -567,152 +593,6 @@ function printImpactAnalysis(graph, nodeId, _flags) {
|
|
|
567
593
|
}
|
|
568
594
|
console.log('');
|
|
569
595
|
}
|
|
570
|
-
function printSearch(graph, term) {
|
|
571
|
-
const matches = searchNodes(graph, term, 30);
|
|
572
|
-
if (matches.length === 0) {
|
|
573
|
-
console.log(`\n No nodes found matching "${term}".\n`);
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
console.log(`\n Search Results for "${term}" (${matches.length} match${matches.length === 1 ? '' : 'es'}):`);
|
|
577
|
-
console.log(' ' + '='.repeat(50));
|
|
578
|
-
for (const { node, score } of matches) {
|
|
579
|
-
const typeLabel = nodeTypeLabel(node.type);
|
|
580
|
-
const domain = node.domain ? ` [${node.domain}]` : '';
|
|
581
|
-
const status = node.status ? ` (${node.status})` : '';
|
|
582
|
-
console.log(` ${typeLabel} ${node.name}${domain}${status}`);
|
|
583
|
-
}
|
|
584
|
-
console.log('\n Use `dql lineage --focus <name>` to see focused lineage for a specific node.\n');
|
|
585
|
-
}
|
|
586
|
-
function printFocused(graph, name, depth) {
|
|
587
|
-
const result = queryLineage(graph, {
|
|
588
|
-
focus: name,
|
|
589
|
-
upstreamDepth: depth,
|
|
590
|
-
downstreamDepth: depth,
|
|
591
|
-
});
|
|
592
|
-
if (!result.focalNode) {
|
|
593
|
-
console.error(`"${name}" not found in lineage graph.`);
|
|
594
|
-
process.exitCode = 1;
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
const focal = result.focalNode;
|
|
598
|
-
const subNodes = result.graph.nodes;
|
|
599
|
-
const subEdges = result.graph.edges;
|
|
600
|
-
const typeLabel = nodeTypeLabel(focal.type);
|
|
601
|
-
console.log(`\n Focused Lineage: ${focal.name} (${typeLabel})`);
|
|
602
|
-
console.log(' ' + '='.repeat(50));
|
|
603
|
-
if (depth !== undefined)
|
|
604
|
-
console.log(` Depth: ${depth} hop(s)`);
|
|
605
|
-
console.log(` Subgraph: ${subNodes.length} nodes, ${subEdges.length} edges`);
|
|
606
|
-
// Separate upstream vs downstream relative to focal
|
|
607
|
-
const focusGraph = LineageGraph.fromJSON(result.graph);
|
|
608
|
-
const upstream = focusGraph.ancestors(focal.id).filter((n) => n.type !== 'domain');
|
|
609
|
-
const downstream = focusGraph.descendants(focal.id).filter((n) => n.type !== 'domain');
|
|
610
|
-
if (upstream.length > 0) {
|
|
611
|
-
console.log(`\n Upstream (${upstream.length}):`);
|
|
612
|
-
for (const n of upstream) {
|
|
613
|
-
const label = nodeTypeLabel(n.type);
|
|
614
|
-
const badge = n.status === 'certified' ? ' [certified]' : '';
|
|
615
|
-
console.log(` ${label} ${n.name}${badge}${n.domain ? ` [${n.domain}]` : ''}`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
console.log(`\n >>> ${typeLabel} ${focal.name} ${focal.status ? `(${focal.status})` : ''} <<<`);
|
|
619
|
-
if (downstream.length > 0) {
|
|
620
|
-
console.log(`\n Downstream (${downstream.length}):`);
|
|
621
|
-
for (const n of downstream) {
|
|
622
|
-
const label = nodeTypeLabel(n.type);
|
|
623
|
-
const badge = n.status === 'certified' ? ' [certified]' : '';
|
|
624
|
-
console.log(` ${label} ${n.name}${badge}${n.domain ? ` [${n.domain}]` : ''}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
// Show data flow tree
|
|
628
|
-
console.log('\n Data Flow:');
|
|
629
|
-
console.log(' ' + '-'.repeat(50));
|
|
630
|
-
const printed = new Set();
|
|
631
|
-
const roots = subNodes.filter((n) => n.type !== 'domain' && focusGraph.getIncomingEdges(n.id).length === 0);
|
|
632
|
-
for (const root of roots.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
633
|
-
const rootNode = focusGraph.getNode(root.id);
|
|
634
|
-
if (rootNode)
|
|
635
|
-
printDAGNode(focusGraph, rootNode, 0, printed);
|
|
636
|
-
}
|
|
637
|
-
console.log('');
|
|
638
|
-
}
|
|
639
|
-
function printDbtDAG(graph) {
|
|
640
|
-
const dbtModels = graph.getNodesByType('dbt_model');
|
|
641
|
-
const dbtSources = graph.getNodesByType('dbt_source');
|
|
642
|
-
if (dbtModels.length === 0 && dbtSources.length === 0) {
|
|
643
|
-
console.log('\n No dbt models found in lineage.');
|
|
644
|
-
console.log(' Run `dql compile --dbt-manifest target/manifest.json` to import dbt lineage.\n');
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
console.log('\n dbt Model DAG');
|
|
648
|
-
console.log(' ' + '='.repeat(50));
|
|
649
|
-
console.log(`\n ${dbtSources.length} source(s), ${dbtModels.length} model(s)`);
|
|
650
|
-
if (dbtSources.length > 0) {
|
|
651
|
-
console.log(`\n Sources:`);
|
|
652
|
-
for (const src of dbtSources.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
653
|
-
const downstream = graph.getOutgoingEdges(src.id)
|
|
654
|
-
.map((e) => graph.getNode(e.target))
|
|
655
|
-
.filter((n) => n !== undefined)
|
|
656
|
-
.map((n) => n.name);
|
|
657
|
-
const arrow = downstream.length > 0 ? ` -> ${downstream.join(', ')}` : '';
|
|
658
|
-
console.log(` SRC ${src.name}${arrow}`);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
if (dbtModels.length > 0) {
|
|
662
|
-
console.log(`\n Models:`);
|
|
663
|
-
for (const model of dbtModels.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
664
|
-
const upstream = graph.getIncomingEdges(model.id)
|
|
665
|
-
.filter((e) => e.type === 'depends_on')
|
|
666
|
-
.map((e) => graph.getNode(e.source))
|
|
667
|
-
.filter((n) => n !== undefined)
|
|
668
|
-
.map((n) => n.name);
|
|
669
|
-
const downstream = graph.getOutgoingEdges(model.id)
|
|
670
|
-
.map((e) => graph.getNode(e.target))
|
|
671
|
-
.filter((n) => n !== undefined)
|
|
672
|
-
.map((n) => `${nodeTypeLabel(n.type)} ${n.name}`);
|
|
673
|
-
const from = upstream.length > 0 ? ` <- ${upstream.join(', ')}` : '';
|
|
674
|
-
console.log(` DBT ${model.name}${from}`);
|
|
675
|
-
if (downstream.length > 0) {
|
|
676
|
-
console.log(` -> ${downstream.join(', ')}`);
|
|
677
|
-
}
|
|
678
|
-
// Show columns if available
|
|
679
|
-
if (model.columns && model.columns.length > 0) {
|
|
680
|
-
const colNames = model.columns.slice(0, 8).map((c) => c.name);
|
|
681
|
-
const more = model.columns.length > 8 ? ` +${model.columns.length - 8} more` : '';
|
|
682
|
-
console.log(` columns: ${colNames.join(', ')}${more}`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
// Show data flow tree
|
|
687
|
-
console.log('\n dbt Data Flow:');
|
|
688
|
-
console.log(' ' + '-'.repeat(50));
|
|
689
|
-
const printed = new Set();
|
|
690
|
-
for (const src of dbtSources.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
691
|
-
printDAGNode(graph, src, 0, printed);
|
|
692
|
-
}
|
|
693
|
-
// Print models not reachable from sources
|
|
694
|
-
for (const model of dbtModels) {
|
|
695
|
-
if (!printed.has(model.id)) {
|
|
696
|
-
printDAGNode(graph, model, 0, printed);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
console.log('');
|
|
700
|
-
}
|
|
701
|
-
/** Short type label for display */
|
|
702
|
-
function nodeTypeLabel(type) {
|
|
703
|
-
switch (type) {
|
|
704
|
-
case 'source_table': return 'TBL ';
|
|
705
|
-
case 'dbt_model': return 'DBT ';
|
|
706
|
-
case 'dbt_source': return 'SRC ';
|
|
707
|
-
case 'block': return 'BLK ';
|
|
708
|
-
case 'metric': return 'MET ';
|
|
709
|
-
case 'dimension': return 'DIM ';
|
|
710
|
-
case 'chart': return 'CHT ';
|
|
711
|
-
case 'dashboard': return 'DASH';
|
|
712
|
-
case 'domain': return 'DOM ';
|
|
713
|
-
default: return type.toUpperCase().padEnd(4);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
596
|
function printTrustChain(graph, fromBlock, toBlock, _flags) {
|
|
717
597
|
const fromId = `block:${fromBlock}`;
|
|
718
598
|
const toId = `block:${toBlock}`;
|