@duckcodeailabs/dql-cli 1.6.4 → 1.6.6
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/apps-api.d.ts +19 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +335 -15
- package/dist/apps-api.js.map +1 -1
- package/dist/args.d.ts +11 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +21 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/index-D_tpetmE.js +3790 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-studio-import.js +23 -4
- package/dist/block-studio-import.js.map +1 -1
- package/dist/commands/agent.d.ts +2 -2
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +78 -13
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +3 -2
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/compile.d.ts +2 -0
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +33 -1
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/import.js +6 -6
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +17 -3
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +113 -10
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/local-runtime.d.ts +8 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +439 -37
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/promote-from-draft.d.ts +4 -4
- package/dist/promote-from-draft.js +8 -8
- package/dist/promote-from-draft.js.map +1 -1
- package/package.json +11 -11
- package/dist/assets/dql-notebook/assets/index-BFUUTIWF.js +0 -3618
package/dist/local-runtime.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, getDialect, Parser
|
|
|
8
8
|
import { load as loadYaml } from 'js-yaml';
|
|
9
9
|
import { listBlockTemplates } from './block-templates.js';
|
|
10
10
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
11
|
-
import { ClaudeProvider, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, defaultMemoryPath, ensureDefaultMemoryFiles, } from '@duckcodeailabs/dql-agent';
|
|
11
|
+
import { ClaudeProvider, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildLocalContextPack, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, recordRuntimeSchemaSnapshot, } from '@duckcodeailabs/dql-agent';
|
|
12
12
|
import { handleAppsApi } from './apps-api.js';
|
|
13
13
|
import { getEffectiveProviderConfig, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
14
14
|
import { DQLAccessDeniedError, activePersonaAppId, assertAppAccess, loadRuntimeApp, runtimeVariables, } from './governance-runtime.js';
|
|
@@ -17,6 +17,7 @@ import { Certifier } from '@duckcodeailabs/dql-governance';
|
|
|
17
17
|
import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
|
|
18
18
|
import { clearBlockStudioImportSessions, createBlockStudioImportSession, deleteBlockStudioImportSession, listBlockStudioImportSessions, loadBlockStudioImportSession, readBlockStudioImportCandidate, updateBlockStudioImportCandidate, writeBlockStudioImportSession, writeBlockStudioImportCandidate, } from './block-studio-import.js';
|
|
19
19
|
import { MetricFlowUnavailableError, compileMetricFlowQuery, hasDbtSemanticManifest, } from './metricflow.js';
|
|
20
|
+
const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
|
|
20
21
|
export async function startLocalServer(opts) {
|
|
21
22
|
const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
|
|
22
23
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
@@ -50,6 +51,7 @@ export async function startLocalServer(opts) {
|
|
|
50
51
|
catch { /* continue without */ }
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
await refreshLocalMetadataCatalog(projectRoot);
|
|
53
55
|
// Auto-register data/ CSV and Parquet files as DuckDB views so semantic layer
|
|
54
56
|
// queries like `FROM orders` resolve without requiring read_csv_auto() in SQL.
|
|
55
57
|
if (connection.driver === 'file' || connection.driver === 'duckdb') {
|
|
@@ -234,13 +236,22 @@ export async function startLocalServer(opts) {
|
|
|
234
236
|
};
|
|
235
237
|
};
|
|
236
238
|
const getSchemaContextForAgent = async (question) => {
|
|
239
|
+
const catalogContext = await buildAgentSchemaContextFromCatalog(projectRoot, question).catch(() => []);
|
|
240
|
+
if (catalogContext.length > 0) {
|
|
241
|
+
const enriched = await enrichAgentSchemaContextWithValueMatches(question, catalogContext, executor, connection);
|
|
242
|
+
recordAgentRuntimeSchemaSnapshot(projectRoot, enriched, 'catalog enriched runtime schema');
|
|
243
|
+
return enriched;
|
|
244
|
+
}
|
|
237
245
|
try {
|
|
238
246
|
const result = await executor.executeQuery(`SELECT table_schema, table_name, column_name, data_type
|
|
239
247
|
FROM information_schema.columns
|
|
240
248
|
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
|
241
|
-
ORDER BY table_schema, table_name, ordinal_position
|
|
249
|
+
ORDER BY table_schema, table_name, ordinal_position
|
|
250
|
+
LIMIT 2000`, [], runtimeVariables({}), connection);
|
|
242
251
|
const schemaContext = buildAgentSchemaContext(question, result.rows);
|
|
243
|
-
|
|
252
|
+
const enriched = await enrichAgentSchemaContextWithValueMatches(question, schemaContext, executor, connection);
|
|
253
|
+
recordAgentRuntimeSchemaSnapshot(projectRoot, enriched, 'information_schema runtime scan');
|
|
254
|
+
return enriched;
|
|
244
255
|
}
|
|
245
256
|
catch {
|
|
246
257
|
return [];
|
|
@@ -400,10 +411,10 @@ export async function startLocalServer(opts) {
|
|
|
400
411
|
throw new Error(message);
|
|
401
412
|
}
|
|
402
413
|
const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver, tableMapping });
|
|
403
|
-
const
|
|
404
|
-
const result = await executor.executeQuery(sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}),
|
|
414
|
+
const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, targetConnection, projectRoot, projectConfig);
|
|
415
|
+
const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
|
|
405
416
|
return {
|
|
406
|
-
sql:
|
|
417
|
+
sql: prepared.sql,
|
|
407
418
|
result: normalizeQueryResult(result),
|
|
408
419
|
chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
|
|
409
420
|
};
|
|
@@ -1383,6 +1394,7 @@ export async function startLocalServer(opts) {
|
|
|
1383
1394
|
return;
|
|
1384
1395
|
}
|
|
1385
1396
|
setBlockStudioStatus(projectRoot, blockPath, newStatus);
|
|
1397
|
+
await refreshLocalMetadataCatalog(projectRoot);
|
|
1386
1398
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1387
1399
|
res.end(serializeJSON({ ok: true, status: newStatus }));
|
|
1388
1400
|
}
|
|
@@ -1502,10 +1514,7 @@ export async function startLocalServer(opts) {
|
|
|
1502
1514
|
return;
|
|
1503
1515
|
}
|
|
1504
1516
|
const result = await certifyBlockStudioSource(source, blockPath);
|
|
1505
|
-
const blockers =
|
|
1506
|
-
...result.checklist.blockers,
|
|
1507
|
-
...result.certification.errors.map((error) => `${error.rule}: ${error.message}`),
|
|
1508
|
-
];
|
|
1517
|
+
const blockers = Array.from(new Set(result.checklist.blockers));
|
|
1509
1518
|
if (!result.certification.certified || blockers.length > 0) {
|
|
1510
1519
|
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1511
1520
|
res.end(serializeJSON({ ok: false, ...result, blockers }));
|
|
@@ -1513,6 +1522,7 @@ export async function startLocalServer(opts) {
|
|
|
1513
1522
|
}
|
|
1514
1523
|
if (blockPath)
|
|
1515
1524
|
setBlockStudioStatus(projectRoot, blockPath, 'certified');
|
|
1525
|
+
await refreshLocalMetadataCatalog(projectRoot);
|
|
1516
1526
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1517
1527
|
res.end(serializeJSON({ ok: true, status: 'certified', ...result }));
|
|
1518
1528
|
}
|
|
@@ -1616,6 +1626,8 @@ export async function startLocalServer(opts) {
|
|
|
1616
1626
|
}
|
|
1617
1627
|
const nextSession = { ...session, candidates: nextCandidates, updatedAt: new Date().toISOString() };
|
|
1618
1628
|
writeBlockStudioImportSession(projectRoot, nextSession);
|
|
1629
|
+
if (saved.length > 0)
|
|
1630
|
+
await refreshLocalMetadataCatalog(projectRoot);
|
|
1619
1631
|
res.writeHead(errors.length > 0 ? 207 : 200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1620
1632
|
res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors }));
|
|
1621
1633
|
}
|
|
@@ -1733,6 +1745,7 @@ export async function startLocalServer(opts) {
|
|
|
1733
1745
|
});
|
|
1734
1746
|
const next = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
|
|
1735
1747
|
writeBlockStudioImportCandidate(projectRoot, importId, next);
|
|
1748
|
+
await refreshLocalMetadataCatalog(projectRoot);
|
|
1736
1749
|
const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
|
|
1737
1750
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1738
1751
|
res.end(serializeJSON({ candidate: next, block: payload }));
|
|
@@ -1871,6 +1884,7 @@ export async function startLocalServer(opts) {
|
|
|
1871
1884
|
}
|
|
1872
1885
|
: undefined,
|
|
1873
1886
|
});
|
|
1887
|
+
await refreshLocalMetadataCatalog(projectRoot);
|
|
1874
1888
|
const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
|
|
1875
1889
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1876
1890
|
res.end(serializeJSON(payload));
|
|
@@ -2939,23 +2953,20 @@ export async function startLocalServer(opts) {
|
|
|
2939
2953
|
return;
|
|
2940
2954
|
}
|
|
2941
2955
|
if (req.method === 'POST' && path === '/api/test-connection') {
|
|
2956
|
+
let target = connection;
|
|
2942
2957
|
try {
|
|
2943
2958
|
const body = await readJSON(req);
|
|
2944
|
-
|
|
2959
|
+
target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
|
|
2945
2960
|
const connector = await executor.getConnector(target);
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
res.
|
|
2949
|
-
res.end(serializeJSON({
|
|
2950
|
-
ok,
|
|
2951
|
-
message: ok ? `Connected to ${driver} successfully` : `Connection to ${driver} failed`,
|
|
2952
|
-
}));
|
|
2961
|
+
const result = await validateConnectionForTest(connector, target);
|
|
2962
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
2963
|
+
res.end(serializeJSON(result));
|
|
2953
2964
|
}
|
|
2954
2965
|
catch (error) {
|
|
2955
|
-
res.writeHead(
|
|
2966
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
2956
2967
|
res.end(serializeJSON({
|
|
2957
2968
|
ok: false,
|
|
2958
|
-
message:
|
|
2969
|
+
message: formatConnectionTestError(target, error),
|
|
2959
2970
|
}));
|
|
2960
2971
|
}
|
|
2961
2972
|
return;
|
|
@@ -3207,7 +3218,9 @@ export async function startLocalServer(opts) {
|
|
|
3207
3218
|
const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
|
|
3208
3219
|
const executableCell = resolved.cell;
|
|
3209
3220
|
const cellConnection = isConnectionConfig(body.connection) ? body.connection : connection;
|
|
3210
|
-
const tableMapping =
|
|
3221
|
+
const tableMapping = needsSemanticTableMapping(executableCell)
|
|
3222
|
+
? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer)
|
|
3223
|
+
: undefined;
|
|
3211
3224
|
const plan = buildExecutionPlan(executableCell, { semanticLayer, driver: cellConnection.driver, tableMapping });
|
|
3212
3225
|
if (!plan) {
|
|
3213
3226
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -3283,7 +3296,10 @@ table: ${table}${tagList}
|
|
|
3283
3296
|
return;
|
|
3284
3297
|
}
|
|
3285
3298
|
const content = readFileSync(filePath);
|
|
3286
|
-
res.writeHead(200, {
|
|
3299
|
+
res.writeHead(200, {
|
|
3300
|
+
'Content-Type': contentTypeFor(filePath),
|
|
3301
|
+
'Cache-Control': 'no-store, max-age=0',
|
|
3302
|
+
});
|
|
3287
3303
|
res.end(content);
|
|
3288
3304
|
});
|
|
3289
3305
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -3328,6 +3344,102 @@ export function formatLocalQueryRuntimeError(connection, error) {
|
|
|
3328
3344
|
}
|
|
3329
3345
|
return `Local query runtime is unavailable for driver "${driver}": ${detail}`;
|
|
3330
3346
|
}
|
|
3347
|
+
export async function validateConnectionForTest(connector, connection) {
|
|
3348
|
+
if (connection.driver === 'snowflake') {
|
|
3349
|
+
return validateSnowflakeConnectionForTest(connector, connection);
|
|
3350
|
+
}
|
|
3351
|
+
const ok = await connector.ping();
|
|
3352
|
+
const label = connectionDriverLabel(connection);
|
|
3353
|
+
return {
|
|
3354
|
+
ok,
|
|
3355
|
+
message: ok
|
|
3356
|
+
? `Connected to ${label} successfully.`
|
|
3357
|
+
: `Connection to ${label} failed. Check credentials, network access, and database availability.`,
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
function formatConnectionTestError(connection, error) {
|
|
3361
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3362
|
+
const label = connectionDriverLabel(connection);
|
|
3363
|
+
if (connection.driver === 'snowflake') {
|
|
3364
|
+
const cleaned = detail.replace(/^Snowflake (?:connection|query) failed:\s*/i, '').trim();
|
|
3365
|
+
return `Snowflake connection failed: ${cleaned || 'Check account, user, password/auth method, role, and network access.'}`;
|
|
3366
|
+
}
|
|
3367
|
+
return `Connection to ${label} failed: ${detail}`;
|
|
3368
|
+
}
|
|
3369
|
+
async function validateSnowflakeConnectionForTest(connector, connection) {
|
|
3370
|
+
const warehouse = connection.warehouse?.trim();
|
|
3371
|
+
if (!warehouse) {
|
|
3372
|
+
return {
|
|
3373
|
+
ok: false,
|
|
3374
|
+
message: 'Snowflake connection requires a warehouse before it can be tested.',
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
const warehouseRow = await findSnowflakeWarehouse(connector, warehouse);
|
|
3378
|
+
if (!warehouseRow) {
|
|
3379
|
+
return {
|
|
3380
|
+
ok: false,
|
|
3381
|
+
message: `Snowflake warehouse "${warehouse}" was not found or is not visible to this role.`,
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
const state = String(readRowField(warehouseRow, 'state') ?? '').trim();
|
|
3385
|
+
const normalizedState = state.toUpperCase();
|
|
3386
|
+
if (normalizedState && normalizedState !== 'STARTED') {
|
|
3387
|
+
return {
|
|
3388
|
+
ok: false,
|
|
3389
|
+
message: `Snowflake warehouse "${warehouse}" is ${state}. Start or resume it, then test again.`,
|
|
3390
|
+
details: {
|
|
3391
|
+
warehouse,
|
|
3392
|
+
state,
|
|
3393
|
+
},
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
const context = await connector.execute(`SELECT
|
|
3397
|
+
CURRENT_ACCOUNT() AS account_name,
|
|
3398
|
+
CURRENT_USER() AS user_name,
|
|
3399
|
+
CURRENT_ROLE() AS role_name,
|
|
3400
|
+
CURRENT_DATABASE() AS database_name,
|
|
3401
|
+
CURRENT_SCHEMA() AS schema_name,
|
|
3402
|
+
CURRENT_WAREHOUSE() AS warehouse_name`);
|
|
3403
|
+
const row = context.rows[0] ?? {};
|
|
3404
|
+
const user = String(readRowField(row, 'user_name') ?? connection.username ?? '').trim();
|
|
3405
|
+
const role = String(readRowField(row, 'role_name') ?? connection.role ?? '').trim();
|
|
3406
|
+
const activeWarehouse = String(readRowField(row, 'warehouse_name') ?? warehouse).trim();
|
|
3407
|
+
return {
|
|
3408
|
+
ok: true,
|
|
3409
|
+
message: `Connected to Snowflake${user ? ` as ${user}` : ''} using warehouse ${activeWarehouse || warehouse}.`,
|
|
3410
|
+
details: {
|
|
3411
|
+
warehouse: activeWarehouse || warehouse,
|
|
3412
|
+
warehouseState: state || 'STARTED',
|
|
3413
|
+
role: role || undefined,
|
|
3414
|
+
database: readRowField(row, 'database_name') ?? connection.database,
|
|
3415
|
+
schema: readRowField(row, 'schema_name') ?? connection.schema,
|
|
3416
|
+
},
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
async function findSnowflakeWarehouse(connector, warehouse) {
|
|
3420
|
+
const candidates = Array.from(new Set([warehouse, warehouse.toUpperCase()]));
|
|
3421
|
+
for (const candidate of candidates) {
|
|
3422
|
+
const result = await connector.execute(`SHOW WAREHOUSES LIKE '${escapeSqlString(candidate)}'`);
|
|
3423
|
+
const row = result.rows.find((item) => {
|
|
3424
|
+
const name = String(readRowField(item, 'name') ?? '').trim();
|
|
3425
|
+
return name.localeCompare(warehouse, undefined, { sensitivity: 'accent' }) === 0;
|
|
3426
|
+
});
|
|
3427
|
+
if (row)
|
|
3428
|
+
return row;
|
|
3429
|
+
}
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
function readRowField(row, field) {
|
|
3433
|
+
const expected = field.toLowerCase();
|
|
3434
|
+
const entry = Object.entries(row).find(([key]) => key.toLowerCase() === expected);
|
|
3435
|
+
return entry?.[1];
|
|
3436
|
+
}
|
|
3437
|
+
function escapeSqlString(value) {
|
|
3438
|
+
return value.replace(/'/g, "''");
|
|
3439
|
+
}
|
|
3440
|
+
function connectionDriverLabel(connection) {
|
|
3441
|
+
return connection.driver === 'snowflake' ? 'Snowflake' : connection.driver ?? 'database';
|
|
3442
|
+
}
|
|
3331
3443
|
/**
|
|
3332
3444
|
* Normalize connector QueryResult → SPA-friendly shape.
|
|
3333
3445
|
* Connector returns columns as ColumnMeta[] ({name,type,driverType}).
|
|
@@ -3336,16 +3448,19 @@ export function formatLocalQueryRuntimeError(connection, error) {
|
|
|
3336
3448
|
function normalizeQueryResult(result, semanticRefs) {
|
|
3337
3449
|
const rawCols = Array.isArray(result?.columns) ? result.columns : [];
|
|
3338
3450
|
const columns = rawCols.map((c) => typeof c === 'string' ? c : typeof c?.name === 'string' ? c.name : String(c));
|
|
3451
|
+
const rawRows = Array.isArray(result?.rows) ? result.rows : [];
|
|
3452
|
+
const rows = rawRows.slice(0, NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT);
|
|
3339
3453
|
const hasRefs = semanticRefs && (semanticRefs.metrics.length > 0 || semanticRefs.dimensions.length > 0);
|
|
3340
3454
|
return {
|
|
3341
3455
|
columns,
|
|
3342
|
-
rows
|
|
3343
|
-
rowCount: typeof result?.rowCount === 'number' ? result.rowCount :
|
|
3456
|
+
rows,
|
|
3457
|
+
rowCount: typeof result?.rowCount === 'number' ? result.rowCount : rawRows.length,
|
|
3344
3458
|
executionTime: typeof result?.executionTimeMs === 'number'
|
|
3345
3459
|
? result.executionTimeMs
|
|
3346
3460
|
: typeof result?.executionTime === 'number'
|
|
3347
3461
|
? result.executionTime
|
|
3348
3462
|
: 0,
|
|
3463
|
+
...(rawRows.length > rows.length ? { truncated: true } : {}),
|
|
3349
3464
|
...(hasRefs ? { semanticRefs } : {}),
|
|
3350
3465
|
};
|
|
3351
3466
|
}
|
|
@@ -3414,6 +3529,15 @@ export function serializeJSON(value) {
|
|
|
3414
3529
|
return current;
|
|
3415
3530
|
});
|
|
3416
3531
|
}
|
|
3532
|
+
async function refreshLocalMetadataCatalog(projectRoot) {
|
|
3533
|
+
try {
|
|
3534
|
+
await ensureMetadataCatalogFresh(projectRoot, { force: true });
|
|
3535
|
+
}
|
|
3536
|
+
catch {
|
|
3537
|
+
// The catalog is a rebuildable local cache. Save/certify flows should not
|
|
3538
|
+
// fail only because metadata refresh hit a stale dbt or semantic config.
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3417
3541
|
function renderNotFound(path) {
|
|
3418
3542
|
return `<!doctype html>
|
|
3419
3543
|
<html lang="en">
|
|
@@ -3551,13 +3675,112 @@ function isPlaceholderLocalConnection(value) {
|
|
|
3551
3675
|
}
|
|
3552
3676
|
export function prepareLocalExecution(sql, connection, projectRoot, projectConfig) {
|
|
3553
3677
|
const normalizedConnection = normalizeProjectConnection(connection, projectRoot);
|
|
3678
|
+
const dbtResolvedSql = resolveDbtMacrosForExecution(sql, projectRoot, projectConfig);
|
|
3554
3679
|
return {
|
|
3555
3680
|
sql: shouldResolveProjectPaths(normalizedConnection)
|
|
3556
|
-
? resolveProjectRelativeSqlPaths(
|
|
3557
|
-
:
|
|
3681
|
+
? resolveProjectRelativeSqlPaths(dbtResolvedSql, projectRoot, projectConfig.dataDir)
|
|
3682
|
+
: dbtResolvedSql,
|
|
3558
3683
|
connection: normalizedConnection,
|
|
3559
3684
|
};
|
|
3560
3685
|
}
|
|
3686
|
+
export function resolveDbtMacrosForExecution(sql, projectRoot, projectConfig = {}) {
|
|
3687
|
+
if (!/\{\{\s*(?:ref|source)\s*\(/i.test(sql))
|
|
3688
|
+
return sql;
|
|
3689
|
+
const manifestPath = resolveDbtManifestPath(projectRoot, projectConfig);
|
|
3690
|
+
if (!manifestPath) {
|
|
3691
|
+
throw new Error('dbt ref/source macros were found, but target/manifest.json was not available. Run dbt parse or dbt compile, then retry.');
|
|
3692
|
+
}
|
|
3693
|
+
const manifest = readJsonFile(manifestPath);
|
|
3694
|
+
const refs = buildDbtRelationLookup(manifest);
|
|
3695
|
+
const unresolved = new Set();
|
|
3696
|
+
let rendered = sql.replace(/\{\{\s*ref\(\s*(?:(['"])([^'"]+)\1\s*,\s*)?(['"])([^'"]+)\3(?:\s*,[^)]*)?\)\s*\}\}/gi, (match, _pkgQuote, packageName, _modelQuote, modelName) => {
|
|
3697
|
+
const key = normalizeDbtLookupKey(modelName);
|
|
3698
|
+
const scopedKey = packageName ? normalizeDbtLookupKey(`${packageName}.${modelName}`) : key;
|
|
3699
|
+
const relation = refs.models.get(scopedKey) ?? refs.models.get(key);
|
|
3700
|
+
if (!relation) {
|
|
3701
|
+
unresolved.add(packageName ? `ref('${packageName}', '${modelName}')` : `ref('${modelName}')`);
|
|
3702
|
+
return match;
|
|
3703
|
+
}
|
|
3704
|
+
return relation;
|
|
3705
|
+
});
|
|
3706
|
+
rendered = rendered.replace(/\{\{\s*source\(\s*(['"])([^'"]+)\1\s*,\s*(['"])([^'"]+)\3\s*\)\s*\}\}/gi, (match, _sourceQuote, sourceName, _tableQuote, tableName) => {
|
|
3707
|
+
const key = normalizeDbtLookupKey(`${sourceName}.${tableName}`);
|
|
3708
|
+
const relation = refs.sources.get(key) ?? refs.sources.get(normalizeDbtLookupKey(tableName));
|
|
3709
|
+
if (!relation) {
|
|
3710
|
+
unresolved.add(`source('${sourceName}', '${tableName}')`);
|
|
3711
|
+
return match;
|
|
3712
|
+
}
|
|
3713
|
+
return relation;
|
|
3714
|
+
});
|
|
3715
|
+
if (unresolved.size > 0) {
|
|
3716
|
+
throw new Error(`Could not resolve dbt macro${unresolved.size === 1 ? '' : 's'} from manifest.json: ${Array.from(unresolved).join(', ')}.`);
|
|
3717
|
+
}
|
|
3718
|
+
return rendered;
|
|
3719
|
+
}
|
|
3720
|
+
function buildDbtRelationLookup(manifest) {
|
|
3721
|
+
const models = new Map();
|
|
3722
|
+
const sources = new Map();
|
|
3723
|
+
const root = manifest && typeof manifest === 'object' ? manifest : {};
|
|
3724
|
+
const nodes = root.nodes && typeof root.nodes === 'object' ? root.nodes : {};
|
|
3725
|
+
const manifestSources = root.sources && typeof root.sources === 'object' ? root.sources : {};
|
|
3726
|
+
for (const [uniqueId, rawNode] of Object.entries(nodes)) {
|
|
3727
|
+
const node = rawNode && typeof rawNode === 'object' ? rawNode : null;
|
|
3728
|
+
if (!node || node.resource_type !== 'model')
|
|
3729
|
+
continue;
|
|
3730
|
+
const relation = dbtRelationName(node);
|
|
3731
|
+
if (!relation)
|
|
3732
|
+
continue;
|
|
3733
|
+
const name = stringField(node, 'name');
|
|
3734
|
+
const alias = stringField(node, 'alias');
|
|
3735
|
+
const packageName = uniqueId.split('.')[1];
|
|
3736
|
+
for (const key of [name, alias, packageName && name ? `${packageName}.${name}` : null, uniqueId]) {
|
|
3737
|
+
if (key)
|
|
3738
|
+
models.set(normalizeDbtLookupKey(key), relation);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
for (const [uniqueId, rawSource] of Object.entries(manifestSources)) {
|
|
3742
|
+
const source = rawSource && typeof rawSource === 'object' ? rawSource : null;
|
|
3743
|
+
if (!source)
|
|
3744
|
+
continue;
|
|
3745
|
+
const relation = dbtRelationName(source);
|
|
3746
|
+
if (!relation)
|
|
3747
|
+
continue;
|
|
3748
|
+
const sourceName = stringField(source, 'source_name');
|
|
3749
|
+
const name = stringField(source, 'name');
|
|
3750
|
+
const identifier = stringField(source, 'identifier');
|
|
3751
|
+
for (const key of [
|
|
3752
|
+
sourceName && name ? `${sourceName}.${name}` : null,
|
|
3753
|
+
sourceName && identifier ? `${sourceName}.${identifier}` : null,
|
|
3754
|
+
name,
|
|
3755
|
+
identifier,
|
|
3756
|
+
uniqueId,
|
|
3757
|
+
]) {
|
|
3758
|
+
if (key)
|
|
3759
|
+
sources.set(normalizeDbtLookupKey(key), relation);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
return { models, sources };
|
|
3763
|
+
}
|
|
3764
|
+
function dbtRelationName(node) {
|
|
3765
|
+
const relationName = stringField(node, 'relation_name');
|
|
3766
|
+
if (relationName)
|
|
3767
|
+
return relationName;
|
|
3768
|
+
const database = stringField(node, 'database');
|
|
3769
|
+
const schema = stringField(node, 'schema');
|
|
3770
|
+
const alias = stringField(node, 'alias') ?? stringField(node, 'identifier') ?? stringField(node, 'name');
|
|
3771
|
+
if (database && schema && alias)
|
|
3772
|
+
return `${database}.${schema}.${alias}`;
|
|
3773
|
+
if (schema && alias)
|
|
3774
|
+
return `${schema}.${alias}`;
|
|
3775
|
+
return alias ?? null;
|
|
3776
|
+
}
|
|
3777
|
+
function stringField(source, key) {
|
|
3778
|
+
const value = source[key];
|
|
3779
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
3780
|
+
}
|
|
3781
|
+
function normalizeDbtLookupKey(value) {
|
|
3782
|
+
return value.trim().replace(/^['"]|['"]$/g, '').toLowerCase();
|
|
3783
|
+
}
|
|
3561
3784
|
const AGENT_PREVIEW_FORBIDDEN_SQL = [
|
|
3562
3785
|
'alter',
|
|
3563
3786
|
'analyze',
|
|
@@ -3677,6 +3900,13 @@ export function prepareSemanticSql(sql, semanticLayer) {
|
|
|
3677
3900
|
unresolvedRefs: resolution.unresolvedRefs,
|
|
3678
3901
|
};
|
|
3679
3902
|
}
|
|
3903
|
+
function needsSemanticTableMapping(cell) {
|
|
3904
|
+
if (cell.type === 'sql')
|
|
3905
|
+
return hasSemanticRefs(cell.source);
|
|
3906
|
+
if (cell.type !== 'dql')
|
|
3907
|
+
return false;
|
|
3908
|
+
return hasSemanticRefs(cell.source) || /\btype\s*=\s*"semantic"/i.test(cell.source);
|
|
3909
|
+
}
|
|
3680
3910
|
export function normalizeProjectConnection(connection, projectRoot) {
|
|
3681
3911
|
const normalized = expandConnectionEnvPlaceholders({ ...connection });
|
|
3682
3912
|
if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
|
|
@@ -4129,7 +4359,9 @@ export async function resolveSemanticTableMapping(executor, connection, semantic
|
|
|
4129
4359
|
try {
|
|
4130
4360
|
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
|
|
4131
4361
|
FROM information_schema.tables
|
|
4132
|
-
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
|
4362
|
+
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
|
4363
|
+
ORDER BY table_schema, table_name
|
|
4364
|
+
LIMIT 2000`, [], {}, connection);
|
|
4133
4365
|
return buildSemanticTableMapping(semanticLayer, tablesResult.rows);
|
|
4134
4366
|
}
|
|
4135
4367
|
catch {
|
|
@@ -4580,12 +4812,6 @@ function buildBlockStudioCertificationChecklist(input) {
|
|
|
4580
4812
|
blockers.add(`${error.rule}: ${error.message}`);
|
|
4581
4813
|
for (const blocker of input.extraBlockers ?? [])
|
|
4582
4814
|
blockers.add(blocker);
|
|
4583
|
-
if (!parsed.domain.trim())
|
|
4584
|
-
blockers.add('Missing domain');
|
|
4585
|
-
if (!parsed.owner.trim())
|
|
4586
|
-
blockers.add('Missing owner');
|
|
4587
|
-
if (!parsed.description.trim())
|
|
4588
|
-
blockers.add('Missing description');
|
|
4589
4815
|
if (!input.previewSucceeded)
|
|
4590
4816
|
blockers.add('Block has not run successfully');
|
|
4591
4817
|
if (!input.testResults || input.testResults.failed > 0)
|
|
@@ -5280,7 +5506,7 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
|
|
|
5280
5506
|
// Fall back to a live build.
|
|
5281
5507
|
}
|
|
5282
5508
|
}
|
|
5283
|
-
const dbtManifestPath = resolveDbtManifestPath(projectRoot);
|
|
5509
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot, {});
|
|
5284
5510
|
try {
|
|
5285
5511
|
const manifest = buildManifest({
|
|
5286
5512
|
projectRoot,
|
|
@@ -5337,9 +5563,14 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
|
|
|
5337
5563
|
return buildLineageGraph(blocks, metrics, dimensions);
|
|
5338
5564
|
}
|
|
5339
5565
|
}
|
|
5340
|
-
function resolveDbtManifestPath(projectRoot) {
|
|
5341
|
-
const
|
|
5342
|
-
|
|
5566
|
+
function resolveDbtManifestPath(projectRoot, projectConfig = {}) {
|
|
5567
|
+
const candidates = [];
|
|
5568
|
+
if (projectConfig.dbt?.projectDir || projectConfig.semanticLayer?.provider === 'dbt') {
|
|
5569
|
+
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
5570
|
+
candidates.push(resolve(dbtProjectPath, projectConfig.dbt?.manifestPath ?? 'target/manifest.json'));
|
|
5571
|
+
}
|
|
5572
|
+
candidates.push(join(projectRoot, 'target', 'manifest.json'), join(resolve(projectRoot, '..'), 'target', 'manifest.json'), join(resolve(projectRoot, '../dbt'), 'target', 'manifest.json'), join(resolve(projectRoot, '../../dbt'), 'target', 'manifest.json'));
|
|
5573
|
+
return candidates.find((candidate, index, list) => list.indexOf(candidate) === index && existsSync(candidate));
|
|
5343
5574
|
}
|
|
5344
5575
|
export function discoverDbtProfileConnections(projectRoot, projectConfig) {
|
|
5345
5576
|
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
@@ -6301,6 +6532,177 @@ function isAiPinRefreshDue(lastRefreshedAt) {
|
|
|
6301
6532
|
return true;
|
|
6302
6533
|
return Date.now() - last >= 24 * 60 * 60 * 1000;
|
|
6303
6534
|
}
|
|
6535
|
+
async function buildAgentSchemaContextFromCatalog(projectRoot, question) {
|
|
6536
|
+
const contextPack = await buildLocalContextPack(projectRoot, { question, limit: 80 });
|
|
6537
|
+
return buildAgentSchemaContextFromContextPack(question, contextPack);
|
|
6538
|
+
}
|
|
6539
|
+
function recordAgentRuntimeSchemaSnapshot(projectRoot, schemaContext, source) {
|
|
6540
|
+
if (schemaContext.length === 0)
|
|
6541
|
+
return;
|
|
6542
|
+
try {
|
|
6543
|
+
recordRuntimeSchemaSnapshot(projectRoot, {
|
|
6544
|
+
source,
|
|
6545
|
+
tables: schemaContext.slice(0, 80).map((table) => ({
|
|
6546
|
+
relation: table.relation,
|
|
6547
|
+
schema: table.schema,
|
|
6548
|
+
name: table.name,
|
|
6549
|
+
description: table.description,
|
|
6550
|
+
source: table.source,
|
|
6551
|
+
columns: table.columns.slice(0, 120).map((column) => ({
|
|
6552
|
+
name: column.name,
|
|
6553
|
+
type: column.type,
|
|
6554
|
+
description: column.description,
|
|
6555
|
+
sampleValues: column.sampleValues?.slice(0, 8),
|
|
6556
|
+
})),
|
|
6557
|
+
})),
|
|
6558
|
+
});
|
|
6559
|
+
}
|
|
6560
|
+
catch {
|
|
6561
|
+
// Runtime schema snapshots are advisory local metadata and must not block answers.
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
function buildAgentSchemaContextFromContextPack(question, contextPack) {
|
|
6565
|
+
const byRelation = new Map();
|
|
6566
|
+
const objectsByKey = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
|
|
6567
|
+
const upsert = (table) => {
|
|
6568
|
+
if (!table.relation || !table.name)
|
|
6569
|
+
return;
|
|
6570
|
+
const key = table.relation.toLowerCase();
|
|
6571
|
+
const existing = byRelation.get(key);
|
|
6572
|
+
if (!existing) {
|
|
6573
|
+
byRelation.set(key, {
|
|
6574
|
+
...table,
|
|
6575
|
+
columns: dedupeAgentSchemaColumns(table.columns).slice(0, 80),
|
|
6576
|
+
});
|
|
6577
|
+
return;
|
|
6578
|
+
}
|
|
6579
|
+
byRelation.set(key, {
|
|
6580
|
+
...existing,
|
|
6581
|
+
description: existing.description ?? table.description,
|
|
6582
|
+
source: existing.source === table.source ? existing.source : 'local metadata catalog',
|
|
6583
|
+
columns: dedupeAgentSchemaColumns([...existing.columns, ...table.columns]).slice(0, 80),
|
|
6584
|
+
});
|
|
6585
|
+
};
|
|
6586
|
+
for (const object of contextPack.objects) {
|
|
6587
|
+
const table = metadataObjectToAgentSchemaTable(object);
|
|
6588
|
+
if (table)
|
|
6589
|
+
upsert(table);
|
|
6590
|
+
}
|
|
6591
|
+
for (const edge of contextPack.edges) {
|
|
6592
|
+
if (edge.edgeType !== 'maps_to_dbt_model' && edge.edgeType !== 'uses_dbt_model')
|
|
6593
|
+
continue;
|
|
6594
|
+
const from = objectsByKey.get(edge.fromKey);
|
|
6595
|
+
const to = objectsByKey.get(edge.toKey);
|
|
6596
|
+
const warehouse = from?.objectType === 'warehouse_table' ? from : null;
|
|
6597
|
+
const dbtModel = to && (to.objectType === 'dbt_model' || to.objectType === 'dbt_source') ? to : null;
|
|
6598
|
+
if (!warehouse || !dbtModel)
|
|
6599
|
+
continue;
|
|
6600
|
+
const warehouseTable = metadataObjectToAgentSchemaTable(warehouse);
|
|
6601
|
+
const modelTable = metadataObjectToAgentSchemaTable(dbtModel);
|
|
6602
|
+
if (!warehouseTable || !modelTable)
|
|
6603
|
+
continue;
|
|
6604
|
+
upsert({
|
|
6605
|
+
...warehouseTable,
|
|
6606
|
+
description: warehouseTable.description ?? modelTable.description,
|
|
6607
|
+
columns: modelTable.columns,
|
|
6608
|
+
source: 'local metadata catalog',
|
|
6609
|
+
});
|
|
6610
|
+
}
|
|
6611
|
+
const tokens = agentSchemaTokens(question);
|
|
6612
|
+
const shouldProbeValues = extractAgentValueSearchTerms(question).length > 0;
|
|
6613
|
+
return Array.from(byRelation.values())
|
|
6614
|
+
.map((table) => ({
|
|
6615
|
+
table,
|
|
6616
|
+
score: scoreAgentSchemaTable(table, tokens) + (shouldProbeValues ? scoreAgentValueProbeTable(table) : 0),
|
|
6617
|
+
}))
|
|
6618
|
+
.filter((entry) => entry.table.columns.length > 0 && entry.score > 0)
|
|
6619
|
+
.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
|
|
6620
|
+
.slice(0, 12)
|
|
6621
|
+
.map((entry) => entry.table);
|
|
6622
|
+
}
|
|
6623
|
+
function metadataObjectToAgentSchemaTable(object) {
|
|
6624
|
+
if (object.objectType === 'dbt_column' || object.objectType === 'runtime_column') {
|
|
6625
|
+
const relation = metadataPayloadString(object, 'relation');
|
|
6626
|
+
const model = metadataPayloadString(object, 'model') ?? relation;
|
|
6627
|
+
if (!model)
|
|
6628
|
+
return null;
|
|
6629
|
+
return {
|
|
6630
|
+
relation: relation ?? model,
|
|
6631
|
+
schema: relation ? relation.split('.').slice(-2, -1)[0] : undefined,
|
|
6632
|
+
name: relation ? relation.split('.').at(-1) ?? model : model,
|
|
6633
|
+
source: 'local metadata catalog',
|
|
6634
|
+
columns: [{
|
|
6635
|
+
name: object.name,
|
|
6636
|
+
type: metadataPayloadString(object, 'type'),
|
|
6637
|
+
description: object.description,
|
|
6638
|
+
}],
|
|
6639
|
+
};
|
|
6640
|
+
}
|
|
6641
|
+
if (object.objectType !== 'dbt_model' && object.objectType !== 'dbt_source' && object.objectType !== 'warehouse_table' && object.objectType !== 'runtime_table') {
|
|
6642
|
+
return null;
|
|
6643
|
+
}
|
|
6644
|
+
const relation = metadataObjectRelation(object);
|
|
6645
|
+
if (!relation)
|
|
6646
|
+
return null;
|
|
6647
|
+
const relationParts = relation.split('.').filter(Boolean);
|
|
6648
|
+
const schema = metadataPayloadString(object, 'schema') ?? (relationParts.length >= 2 ? relationParts[relationParts.length - 2] : undefined);
|
|
6649
|
+
const name = relationParts.at(-1) ?? object.name;
|
|
6650
|
+
const columns = metadataObjectColumns(object);
|
|
6651
|
+
return {
|
|
6652
|
+
relation,
|
|
6653
|
+
schema,
|
|
6654
|
+
name,
|
|
6655
|
+
description: object.description,
|
|
6656
|
+
columns,
|
|
6657
|
+
source: 'local metadata catalog',
|
|
6658
|
+
};
|
|
6659
|
+
}
|
|
6660
|
+
function metadataObjectRelation(object) {
|
|
6661
|
+
const relation = metadataPayloadString(object, 'relation');
|
|
6662
|
+
if (relation)
|
|
6663
|
+
return relation;
|
|
6664
|
+
const database = metadataPayloadString(object, 'database');
|
|
6665
|
+
const schema = metadataPayloadString(object, 'schema');
|
|
6666
|
+
if (database && schema)
|
|
6667
|
+
return [database, schema, object.name].join('.');
|
|
6668
|
+
return object.fullName ?? object.name;
|
|
6669
|
+
}
|
|
6670
|
+
function metadataObjectColumns(object) {
|
|
6671
|
+
const columns = object.payload?.columns;
|
|
6672
|
+
if (!Array.isArray(columns))
|
|
6673
|
+
return [];
|
|
6674
|
+
return columns.flatMap((column) => {
|
|
6675
|
+
if (!column || typeof column !== 'object')
|
|
6676
|
+
return [];
|
|
6677
|
+
const record = column;
|
|
6678
|
+
const name = stringFromRecord(record, 'name') ?? stringFromRecord(record, 'column_name');
|
|
6679
|
+
if (!name)
|
|
6680
|
+
return [];
|
|
6681
|
+
return [{
|
|
6682
|
+
name,
|
|
6683
|
+
type: stringFromRecord(record, 'type') ?? stringFromRecord(record, 'data_type'),
|
|
6684
|
+
description: stringFromRecord(record, 'description'),
|
|
6685
|
+
}];
|
|
6686
|
+
});
|
|
6687
|
+
}
|
|
6688
|
+
function metadataPayloadString(object, key) {
|
|
6689
|
+
const value = object.payload?.[key];
|
|
6690
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
6691
|
+
}
|
|
6692
|
+
function dedupeAgentSchemaColumns(columns) {
|
|
6693
|
+
const byName = new Map();
|
|
6694
|
+
for (const column of columns) {
|
|
6695
|
+
const key = column.name.toLowerCase();
|
|
6696
|
+
const existing = byName.get(key);
|
|
6697
|
+
byName.set(key, existing ? {
|
|
6698
|
+
...existing,
|
|
6699
|
+
type: existing.type ?? column.type,
|
|
6700
|
+
description: existing.description ?? column.description,
|
|
6701
|
+
sampleValues: uniqueStrings([...(existing.sampleValues ?? []), ...(column.sampleValues ?? [])]).slice(0, 5),
|
|
6702
|
+
} : column);
|
|
6703
|
+
}
|
|
6704
|
+
return Array.from(byName.values());
|
|
6705
|
+
}
|
|
6304
6706
|
export function buildAgentSchemaContext(question, rows) {
|
|
6305
6707
|
const byRelation = new Map();
|
|
6306
6708
|
for (const row of rows) {
|