@duckcodeailabs/dql-cli 1.6.5 → 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.
@@ -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`, [], runtimeVariables({}), connection);
249
+ ORDER BY table_schema, table_name, ordinal_position
250
+ LIMIT 2000`, [], runtimeVariables({}), connection);
242
251
  const schemaContext = buildAgentSchemaContext(question, result.rows);
243
- return enrichAgentSchemaContextWithValueMatches(question, schemaContext, executor, connection);
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 [];
@@ -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
  }
@@ -1510,6 +1522,7 @@ export async function startLocalServer(opts) {
1510
1522
  }
1511
1523
  if (blockPath)
1512
1524
  setBlockStudioStatus(projectRoot, blockPath, 'certified');
1525
+ await refreshLocalMetadataCatalog(projectRoot);
1513
1526
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1514
1527
  res.end(serializeJSON({ ok: true, status: 'certified', ...result }));
1515
1528
  }
@@ -1613,6 +1626,8 @@ export async function startLocalServer(opts) {
1613
1626
  }
1614
1627
  const nextSession = { ...session, candidates: nextCandidates, updatedAt: new Date().toISOString() };
1615
1628
  writeBlockStudioImportSession(projectRoot, nextSession);
1629
+ if (saved.length > 0)
1630
+ await refreshLocalMetadataCatalog(projectRoot);
1616
1631
  res.writeHead(errors.length > 0 ? 207 : 200, { 'Content-Type': 'application/json; charset=utf-8' });
1617
1632
  res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors }));
1618
1633
  }
@@ -1730,6 +1745,7 @@ export async function startLocalServer(opts) {
1730
1745
  });
1731
1746
  const next = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
1732
1747
  writeBlockStudioImportCandidate(projectRoot, importId, next);
1748
+ await refreshLocalMetadataCatalog(projectRoot);
1733
1749
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
1734
1750
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1735
1751
  res.end(serializeJSON({ candidate: next, block: payload }));
@@ -1868,6 +1884,7 @@ export async function startLocalServer(opts) {
1868
1884
  }
1869
1885
  : undefined,
1870
1886
  });
1887
+ await refreshLocalMetadataCatalog(projectRoot);
1871
1888
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
1872
1889
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1873
1890
  res.end(serializeJSON(payload));
@@ -3201,7 +3218,9 @@ export async function startLocalServer(opts) {
3201
3218
  const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
3202
3219
  const executableCell = resolved.cell;
3203
3220
  const cellConnection = isConnectionConfig(body.connection) ? body.connection : connection;
3204
- const tableMapping = await resolveSemanticTableMapping(executor, cellConnection, semanticLayer);
3221
+ const tableMapping = needsSemanticTableMapping(executableCell)
3222
+ ? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer)
3223
+ : undefined;
3205
3224
  const plan = buildExecutionPlan(executableCell, { semanticLayer, driver: cellConnection.driver, tableMapping });
3206
3225
  if (!plan) {
3207
3226
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -3277,7 +3296,10 @@ table: ${table}${tagList}
3277
3296
  return;
3278
3297
  }
3279
3298
  const content = readFileSync(filePath);
3280
- res.writeHead(200, { 'Content-Type': contentTypeFor(filePath) });
3299
+ res.writeHead(200, {
3300
+ 'Content-Type': contentTypeFor(filePath),
3301
+ 'Cache-Control': 'no-store, max-age=0',
3302
+ });
3281
3303
  res.end(content);
3282
3304
  });
3283
3305
  return new Promise((resolvePromise, reject) => {
@@ -3426,16 +3448,19 @@ function connectionDriverLabel(connection) {
3426
3448
  function normalizeQueryResult(result, semanticRefs) {
3427
3449
  const rawCols = Array.isArray(result?.columns) ? result.columns : [];
3428
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);
3429
3453
  const hasRefs = semanticRefs && (semanticRefs.metrics.length > 0 || semanticRefs.dimensions.length > 0);
3430
3454
  return {
3431
3455
  columns,
3432
- rows: Array.isArray(result?.rows) ? result.rows : [],
3433
- rowCount: typeof result?.rowCount === 'number' ? result.rowCount : (result?.rows?.length ?? 0),
3456
+ rows,
3457
+ rowCount: typeof result?.rowCount === 'number' ? result.rowCount : rawRows.length,
3434
3458
  executionTime: typeof result?.executionTimeMs === 'number'
3435
3459
  ? result.executionTimeMs
3436
3460
  : typeof result?.executionTime === 'number'
3437
3461
  ? result.executionTime
3438
3462
  : 0,
3463
+ ...(rawRows.length > rows.length ? { truncated: true } : {}),
3439
3464
  ...(hasRefs ? { semanticRefs } : {}),
3440
3465
  };
3441
3466
  }
@@ -3504,6 +3529,15 @@ export function serializeJSON(value) {
3504
3529
  return current;
3505
3530
  });
3506
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
+ }
3507
3541
  function renderNotFound(path) {
3508
3542
  return `<!doctype html>
3509
3543
  <html lang="en">
@@ -3866,6 +3900,13 @@ export function prepareSemanticSql(sql, semanticLayer) {
3866
3900
  unresolvedRefs: resolution.unresolvedRefs,
3867
3901
  };
3868
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
+ }
3869
3910
  export function normalizeProjectConnection(connection, projectRoot) {
3870
3911
  const normalized = expandConnectionEnvPlaceholders({ ...connection });
3871
3912
  if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
@@ -4318,7 +4359,9 @@ export async function resolveSemanticTableMapping(executor, connection, semantic
4318
4359
  try {
4319
4360
  const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
4320
4361
  FROM information_schema.tables
4321
- WHERE table_schema NOT IN ('information_schema', 'pg_catalog')`, [], {}, connection);
4362
+ WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
4363
+ ORDER BY table_schema, table_name
4364
+ LIMIT 2000`, [], {}, connection);
4322
4365
  return buildSemanticTableMapping(semanticLayer, tablesResult.rows);
4323
4366
  }
4324
4367
  catch {
@@ -6489,6 +6532,177 @@ function isAiPinRefreshDue(lastRefreshedAt) {
6489
6532
  return true;
6490
6533
  return Date.now() - last >= 24 * 60 * 60 * 1000;
6491
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
+ }
6492
6706
  export function buildAgentSchemaContext(question, rows) {
6493
6707
  const byRelation = new Map();
6494
6708
  for (const row of rows) {