@duckcodeailabs/dql-cli 0.8.8 → 0.8.10
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/LICENSE +123 -0
- package/dist/assets/dql-notebook/assets/index-DIVTsVNu.js +564 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/lineage.d.ts +0 -4
- package/dist/commands/lineage.d.ts.map +1 -1
- package/dist/commands/lineage.js +5 -192
- package/dist/commands/lineage.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/local-runtime.d.ts +24 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +208 -98
- package/dist/local-runtime.js.map +1 -1
- package/dist/local-runtime.test.js +92 -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 +16 -17
- package/dist/assets/dql-notebook/assets/index-Cp34wXvX.js +0 -558
- package/dist/package.json +0 -45
package/dist/local-runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
4
4
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, } from '@duckcodeailabs/dql-notebook';
|
|
5
|
-
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview,
|
|
5
|
+
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, } from '@duckcodeailabs/dql-core';
|
|
6
6
|
import { listBlockTemplates } from './block-templates.js';
|
|
7
7
|
import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
|
|
8
8
|
export async function startLocalServer(opts) {
|
|
@@ -595,17 +595,37 @@ export async function startLocalServer(opts) {
|
|
|
595
595
|
try {
|
|
596
596
|
const body = await readJSON(req);
|
|
597
597
|
const source = typeof body.source === 'string' ? body.source : '';
|
|
598
|
+
const targetConnection = isConnectionConfig(body.connection) ? body.connection : connection;
|
|
599
|
+
let tableMapping;
|
|
600
|
+
if (semanticLayer) {
|
|
601
|
+
try {
|
|
602
|
+
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
|
|
603
|
+
FROM information_schema.tables
|
|
604
|
+
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')`, [], {}, targetConnection);
|
|
605
|
+
tableMapping = buildSemanticTableMapping(semanticLayer, tablesResult.rows);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
tableMapping = undefined;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const semanticCompose = semanticLayer
|
|
612
|
+
? composeSemanticBlockSql(source, semanticLayer, { driver: targetConnection.driver, tableMapping })
|
|
613
|
+
: null;
|
|
598
614
|
const validation = validateBlockStudioSource(source, semanticLayer);
|
|
599
|
-
|
|
615
|
+
const executableSql = semanticCompose?.sql ?? validation.executableSql;
|
|
616
|
+
if (!executableSql) {
|
|
600
617
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
601
|
-
|
|
618
|
+
const message = semanticCompose?.diagnostics.find((item) => item.severity === 'error')?.message
|
|
619
|
+
?? validation.diagnostics.find((item) => item.severity === 'error')?.message
|
|
620
|
+
?? 'No executable SQL found in block source.';
|
|
621
|
+
res.end(serializeJSON({ error: message, diagnostics: validation.diagnostics }));
|
|
602
622
|
return;
|
|
603
623
|
}
|
|
604
|
-
const sql = resolveProjectRelativeSqlPaths(
|
|
605
|
-
const result = await executor.executeQuery(sql, [], {},
|
|
624
|
+
const sql = resolveProjectRelativeSqlPaths(executableSql, projectRoot, projectConfig.dataDir);
|
|
625
|
+
const result = await executor.executeQuery(sql, [], {}, targetConnection);
|
|
606
626
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
607
627
|
res.end(serializeJSON({
|
|
608
|
-
sql:
|
|
628
|
+
sql: executableSql,
|
|
609
629
|
result: normalizeQueryResult(result),
|
|
610
630
|
chartConfig: validation.chartConfig ?? null,
|
|
611
631
|
}));
|
|
@@ -1527,83 +1547,6 @@ export async function startLocalServer(opts) {
|
|
|
1527
1547
|
}
|
|
1528
1548
|
return;
|
|
1529
1549
|
}
|
|
1530
|
-
// ---- Lineage Query API (focused/searchable views) ----
|
|
1531
|
-
if (req.method === 'GET' && path === '/api/lineage/query') {
|
|
1532
|
-
try {
|
|
1533
|
-
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1534
|
-
const focus = url.searchParams.get('focus') ?? undefined;
|
|
1535
|
-
const search = url.searchParams.get('search') ?? undefined;
|
|
1536
|
-
const typesParam = url.searchParams.get('types');
|
|
1537
|
-
const domain = url.searchParams.get('domain') ?? undefined;
|
|
1538
|
-
const upstreamDepth = url.searchParams.has('upstreamDepth')
|
|
1539
|
-
? Number(url.searchParams.get('upstreamDepth'))
|
|
1540
|
-
: undefined;
|
|
1541
|
-
const downstreamDepth = url.searchParams.has('downstreamDepth')
|
|
1542
|
-
? Number(url.searchParams.get('downstreamDepth'))
|
|
1543
|
-
: undefined;
|
|
1544
|
-
const types = typesParam
|
|
1545
|
-
? typesParam.split(',').map((t) => t.trim())
|
|
1546
|
-
: undefined;
|
|
1547
|
-
const result = queryLineage(graph, {
|
|
1548
|
-
focus,
|
|
1549
|
-
search,
|
|
1550
|
-
types,
|
|
1551
|
-
domain,
|
|
1552
|
-
upstreamDepth,
|
|
1553
|
-
downstreamDepth,
|
|
1554
|
-
});
|
|
1555
|
-
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1556
|
-
res.end(serializeJSON(result));
|
|
1557
|
-
}
|
|
1558
|
-
catch (error) {
|
|
1559
|
-
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1560
|
-
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1561
|
-
}
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
if (req.method === 'GET' && path === '/api/lineage/search') {
|
|
1565
|
-
const q = url.searchParams.get('q') ?? '';
|
|
1566
|
-
if (!q) {
|
|
1567
|
-
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1568
|
-
res.end(serializeJSON({ error: 'Missing "q" query parameter' }));
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
try {
|
|
1572
|
-
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1573
|
-
const limit = url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : 20;
|
|
1574
|
-
const matches = searchNodes(graph, q, limit);
|
|
1575
|
-
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1576
|
-
res.end(serializeJSON({ matches }));
|
|
1577
|
-
}
|
|
1578
|
-
catch (error) {
|
|
1579
|
-
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1580
|
-
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1581
|
-
}
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
if (req.method === 'GET' && path.startsWith('/api/lineage/node/')) {
|
|
1585
|
-
const nodeId = decodeURIComponent(path.slice('/api/lineage/node/'.length));
|
|
1586
|
-
try {
|
|
1587
|
-
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1588
|
-
const node = graph.getNode(nodeId);
|
|
1589
|
-
if (!node) {
|
|
1590
|
-
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1591
|
-
res.end(serializeJSON({ error: `Node "${nodeId}" not found` }));
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
const ancestors = graph.ancestors(nodeId);
|
|
1595
|
-
const descendants = graph.descendants(nodeId);
|
|
1596
|
-
const incoming = graph.getIncomingEdges(nodeId);
|
|
1597
|
-
const outgoing = graph.getOutgoingEdges(nodeId);
|
|
1598
|
-
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1599
|
-
res.end(serializeJSON({ node, ancestors, descendants, incoming, outgoing }));
|
|
1600
|
-
}
|
|
1601
|
-
catch (error) {
|
|
1602
|
-
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1603
|
-
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1604
|
-
}
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
1550
|
if (req.method === 'GET' && path === '/api/notebook/bootstrap') {
|
|
1608
1551
|
const welcomeNotebook = resolveNotebook(projectRoot, projectConfig.project ?? 'DQL Project');
|
|
1609
1552
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -2215,21 +2158,183 @@ function openBlockStudioDocument(projectRoot, relativePath, semanticLayer) {
|
|
|
2215
2158
|
validation: validateBlockStudioSource(source, semanticLayer),
|
|
2216
2159
|
};
|
|
2217
2160
|
}
|
|
2218
|
-
function
|
|
2161
|
+
function parseBlockStudioArrayField(source, key) {
|
|
2162
|
+
const match = source.match(new RegExp(`\\b${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'i'));
|
|
2163
|
+
if (!match)
|
|
2164
|
+
return [];
|
|
2165
|
+
return (match[1].match(/"([^"]*)"/g) ?? []).map((value) => value.slice(1, -1)).filter(Boolean);
|
|
2166
|
+
}
|
|
2167
|
+
function parseBlockStudioStringField(source, key) {
|
|
2168
|
+
return source.match(new RegExp(`\\b${key}\\s*=\\s*"([^"]*)"`, 'i'))?.[1] ?? undefined;
|
|
2169
|
+
}
|
|
2170
|
+
function parseSemanticBlockConfig(source) {
|
|
2171
|
+
const blockType = (parseBlockStudioStringField(source, 'type') ?? 'custom').toLowerCase() === 'semantic'
|
|
2172
|
+
? 'semantic'
|
|
2173
|
+
: 'custom';
|
|
2174
|
+
const metric = parseBlockStudioStringField(source, 'metric');
|
|
2175
|
+
const metrics = parseBlockStudioArrayField(source, 'metrics');
|
|
2176
|
+
const dimensions = parseBlockStudioArrayField(source, 'dimensions');
|
|
2177
|
+
const timeDimension = parseBlockStudioStringField(source, 'time_dimension');
|
|
2178
|
+
const granularity = parseBlockStudioStringField(source, 'granularity');
|
|
2179
|
+
const limitMatch = source.match(/\blimit\s*=\s*(\d+)/i);
|
|
2180
|
+
return {
|
|
2181
|
+
blockType,
|
|
2182
|
+
metric,
|
|
2183
|
+
metrics,
|
|
2184
|
+
dimensions,
|
|
2185
|
+
timeDimension,
|
|
2186
|
+
granularity,
|
|
2187
|
+
limit: limitMatch ? Number.parseInt(limitMatch[1], 10) : undefined,
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
function buildSemanticTableMapping(semanticLayer, rows) {
|
|
2191
|
+
const dbTableNames = new Set();
|
|
2192
|
+
const schemaQualified = new Map();
|
|
2193
|
+
for (const row of rows) {
|
|
2194
|
+
const schema = String(row['table_schema'] ?? '');
|
|
2195
|
+
const name = String(row['table_name'] ?? '');
|
|
2196
|
+
if (!name)
|
|
2197
|
+
continue;
|
|
2198
|
+
dbTableNames.add(name);
|
|
2199
|
+
schemaQualified.set(name, schema ? `${schema}.${name}` : name);
|
|
2200
|
+
}
|
|
2201
|
+
const tableMapping = {};
|
|
2202
|
+
const allSemanticTables = new Set();
|
|
2203
|
+
for (const metric of semanticLayer.listMetrics())
|
|
2204
|
+
allSemanticTables.add(metric.table);
|
|
2205
|
+
for (const dimension of semanticLayer.listDimensions())
|
|
2206
|
+
allSemanticTables.add(dimension.table);
|
|
2207
|
+
for (const semTable of allSemanticTables) {
|
|
2208
|
+
if (dbTableNames.has(semTable) && schemaQualified.has(semTable)) {
|
|
2209
|
+
tableMapping[semTable] = schemaQualified.get(semTable);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return Object.keys(tableMapping).length > 0 ? tableMapping : undefined;
|
|
2213
|
+
}
|
|
2214
|
+
function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
2215
|
+
const config = parseSemanticBlockConfig(source);
|
|
2216
|
+
const metrics = config.metrics.length > 0
|
|
2217
|
+
? config.metrics
|
|
2218
|
+
: config.metric
|
|
2219
|
+
? [config.metric]
|
|
2220
|
+
: [];
|
|
2221
|
+
const semanticRefs = {
|
|
2222
|
+
metrics,
|
|
2223
|
+
dimensions: config.dimensions,
|
|
2224
|
+
segments: [],
|
|
2225
|
+
};
|
|
2219
2226
|
const diagnostics = [];
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
parser.parse();
|
|
2227
|
+
if (config.blockType !== 'semantic') {
|
|
2228
|
+
return { sql: null, diagnostics, semanticRefs };
|
|
2223
2229
|
}
|
|
2224
|
-
|
|
2230
|
+
if (metrics.length === 0) {
|
|
2225
2231
|
diagnostics.push({
|
|
2226
2232
|
severity: 'error',
|
|
2227
|
-
code: '
|
|
2228
|
-
message:
|
|
2233
|
+
code: 'semantic_metric_missing',
|
|
2234
|
+
message: 'Semantic block is missing a metric. Add metric = "metric_name" or metrics = ["metric_name"].',
|
|
2229
2235
|
});
|
|
2236
|
+
return { sql: null, diagnostics, semanticRefs };
|
|
2230
2237
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2238
|
+
if (config.timeDimension && !config.granularity) {
|
|
2239
|
+
diagnostics.push({
|
|
2240
|
+
severity: 'error',
|
|
2241
|
+
code: 'semantic_granularity_missing',
|
|
2242
|
+
message: `Semantic block selects time_dimension = "${config.timeDimension}" but is missing granularity.`,
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
const refValidation = semanticLayer.validateReferences([...metrics, ...config.dimensions]);
|
|
2246
|
+
for (const unknown of refValidation.unknown) {
|
|
2247
|
+
diagnostics.push({
|
|
2248
|
+
severity: 'error',
|
|
2249
|
+
code: 'semantic_ref',
|
|
2250
|
+
message: `Unknown semantic reference: ${unknown}`,
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
2254
|
+
return { sql: null, diagnostics, semanticRefs };
|
|
2255
|
+
}
|
|
2256
|
+
const composed = semanticLayer.composeQuery({
|
|
2257
|
+
metrics,
|
|
2258
|
+
dimensions: config.dimensions,
|
|
2259
|
+
timeDimension: config.timeDimension && config.granularity
|
|
2260
|
+
? { name: config.timeDimension, granularity: config.granularity }
|
|
2261
|
+
: undefined,
|
|
2262
|
+
limit: config.limit,
|
|
2263
|
+
driver: options?.driver,
|
|
2264
|
+
tableMapping: options?.tableMapping,
|
|
2265
|
+
});
|
|
2266
|
+
if (!composed) {
|
|
2267
|
+
diagnostics.push({
|
|
2268
|
+
severity: 'error',
|
|
2269
|
+
code: 'semantic_compose_failed',
|
|
2270
|
+
message: `Could not compose SQL for semantic block metrics: [${metrics.join(', ')}].`,
|
|
2271
|
+
});
|
|
2272
|
+
return { sql: null, diagnostics, semanticRefs };
|
|
2273
|
+
}
|
|
2274
|
+
return {
|
|
2275
|
+
sql: composed.sql,
|
|
2276
|
+
diagnostics,
|
|
2277
|
+
semanticRefs,
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
export function validateBlockStudioSource(source, semanticLayer) {
|
|
2281
|
+
const diagnostics = [];
|
|
2282
|
+
const semanticConfig = parseSemanticBlockConfig(source);
|
|
2283
|
+
if (semanticConfig.blockType !== 'semantic') {
|
|
2284
|
+
try {
|
|
2285
|
+
const parser = new Parser(source, '<block-studio>');
|
|
2286
|
+
parser.parse();
|
|
2287
|
+
}
|
|
2288
|
+
catch (error) {
|
|
2289
|
+
diagnostics.push({
|
|
2290
|
+
severity: 'error',
|
|
2291
|
+
code: 'syntax',
|
|
2292
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
else {
|
|
2297
|
+
const hasBlockHeader = /\bblock\s+"[^"]+"\s*\{/i.test(source);
|
|
2298
|
+
const hasClosingBrace = /\}\s*$/m.test(source);
|
|
2299
|
+
if (!hasBlockHeader || !hasClosingBrace) {
|
|
2300
|
+
diagnostics.push({
|
|
2301
|
+
severity: 'error',
|
|
2302
|
+
code: 'semantic_shape',
|
|
2303
|
+
message: 'Semantic block must use block "Name" { ... } structure.',
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
let semanticRefs = extractBlockStudioSemanticReferences(source);
|
|
2308
|
+
if (semanticConfig.blockType === 'semantic') {
|
|
2309
|
+
const selectedMetrics = semanticConfig.metrics.length > 0
|
|
2310
|
+
? semanticConfig.metrics
|
|
2311
|
+
: semanticConfig.metric
|
|
2312
|
+
? [semanticConfig.metric]
|
|
2313
|
+
: [];
|
|
2314
|
+
semanticRefs = {
|
|
2315
|
+
metrics: selectedMetrics,
|
|
2316
|
+
dimensions: semanticConfig.dimensions,
|
|
2317
|
+
segments: semanticRefs.segments,
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
let executableSql = extractBlockStudioSql(source);
|
|
2321
|
+
if (semanticConfig.blockType === 'semantic') {
|
|
2322
|
+
if (semanticLayer) {
|
|
2323
|
+
const semanticCompose = composeSemanticBlockSql(source, semanticLayer);
|
|
2324
|
+
semanticRefs = semanticCompose.semanticRefs;
|
|
2325
|
+
diagnostics.push(...semanticCompose.diagnostics);
|
|
2326
|
+
executableSql = semanticCompose.sql;
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
diagnostics.push({
|
|
2330
|
+
severity: 'error',
|
|
2331
|
+
code: 'semantic_layer_missing',
|
|
2332
|
+
message: 'Semantic block cannot run because no semantic layer is configured.',
|
|
2333
|
+
});
|
|
2334
|
+
executableSql = null;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
else if (semanticLayer) {
|
|
2233
2338
|
const refValidation = semanticLayer.validateReferences([
|
|
2234
2339
|
...semanticRefs.metrics,
|
|
2235
2340
|
...semanticRefs.dimensions,
|
|
@@ -2251,13 +2356,18 @@ function validateBlockStudioSource(source, semanticLayer) {
|
|
|
2251
2356
|
message: 'Block has no visualization section yet.',
|
|
2252
2357
|
});
|
|
2253
2358
|
}
|
|
2254
|
-
const executableSql = extractBlockStudioSql(source);
|
|
2255
2359
|
if (!executableSql) {
|
|
2256
|
-
diagnostics.push(
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2360
|
+
diagnostics.push(semanticConfig.blockType === 'semantic'
|
|
2361
|
+
? {
|
|
2362
|
+
severity: 'warning',
|
|
2363
|
+
code: 'semantic_not_runnable',
|
|
2364
|
+
message: 'Semantic block is not runnable yet. Select a metric and complete any required time settings.',
|
|
2365
|
+
}
|
|
2366
|
+
: {
|
|
2367
|
+
severity: 'warning',
|
|
2368
|
+
code: 'sql_missing',
|
|
2369
|
+
message: 'No executable SQL found in the block source.',
|
|
2370
|
+
});
|
|
2261
2371
|
}
|
|
2262
2372
|
return {
|
|
2263
2373
|
valid: diagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
|