@duckcodeailabs/dql-cli 1.6.4 → 1.6.5

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.
@@ -400,10 +400,10 @@ export async function startLocalServer(opts) {
400
400
  throw new Error(message);
401
401
  }
402
402
  const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver, tableMapping });
403
- const sql = resolveProjectRelativeSqlPaths(semanticCompose?.sql ?? plan?.sql ?? executableSql, projectRoot, projectConfig.dataDir);
404
- const result = await executor.executeQuery(sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), targetConnection);
403
+ const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan?.sql ?? executableSql, targetConnection, projectRoot, projectConfig);
404
+ const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
405
405
  return {
406
- sql: plan?.sql ?? executableSql,
406
+ sql: prepared.sql,
407
407
  result: normalizeQueryResult(result),
408
408
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
409
409
  };
@@ -1502,10 +1502,7 @@ export async function startLocalServer(opts) {
1502
1502
  return;
1503
1503
  }
1504
1504
  const result = await certifyBlockStudioSource(source, blockPath);
1505
- const blockers = [
1506
- ...result.checklist.blockers,
1507
- ...result.certification.errors.map((error) => `${error.rule}: ${error.message}`),
1508
- ];
1505
+ const blockers = Array.from(new Set(result.checklist.blockers));
1509
1506
  if (!result.certification.certified || blockers.length > 0) {
1510
1507
  res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
1511
1508
  res.end(serializeJSON({ ok: false, ...result, blockers }));
@@ -2939,23 +2936,20 @@ export async function startLocalServer(opts) {
2939
2936
  return;
2940
2937
  }
2941
2938
  if (req.method === 'POST' && path === '/api/test-connection') {
2939
+ let target = connection;
2942
2940
  try {
2943
2941
  const body = await readJSON(req);
2944
- const target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
2942
+ target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
2945
2943
  const connector = await executor.getConnector(target);
2946
- const ok = await connector.ping();
2947
- const driver = target.driver ?? 'unknown';
2948
- res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
2949
- res.end(serializeJSON({
2950
- ok,
2951
- message: ok ? `Connected to ${driver} successfully` : `Connection to ${driver} failed`,
2952
- }));
2944
+ const result = await validateConnectionForTest(connector, target);
2945
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2946
+ res.end(serializeJSON(result));
2953
2947
  }
2954
2948
  catch (error) {
2955
- res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
2949
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2956
2950
  res.end(serializeJSON({
2957
2951
  ok: false,
2958
- message: error instanceof Error ? error.message : String(error),
2952
+ message: formatConnectionTestError(target, error),
2959
2953
  }));
2960
2954
  }
2961
2955
  return;
@@ -3328,6 +3322,102 @@ export function formatLocalQueryRuntimeError(connection, error) {
3328
3322
  }
3329
3323
  return `Local query runtime is unavailable for driver "${driver}": ${detail}`;
3330
3324
  }
3325
+ export async function validateConnectionForTest(connector, connection) {
3326
+ if (connection.driver === 'snowflake') {
3327
+ return validateSnowflakeConnectionForTest(connector, connection);
3328
+ }
3329
+ const ok = await connector.ping();
3330
+ const label = connectionDriverLabel(connection);
3331
+ return {
3332
+ ok,
3333
+ message: ok
3334
+ ? `Connected to ${label} successfully.`
3335
+ : `Connection to ${label} failed. Check credentials, network access, and database availability.`,
3336
+ };
3337
+ }
3338
+ function formatConnectionTestError(connection, error) {
3339
+ const detail = error instanceof Error ? error.message : String(error);
3340
+ const label = connectionDriverLabel(connection);
3341
+ if (connection.driver === 'snowflake') {
3342
+ const cleaned = detail.replace(/^Snowflake (?:connection|query) failed:\s*/i, '').trim();
3343
+ return `Snowflake connection failed: ${cleaned || 'Check account, user, password/auth method, role, and network access.'}`;
3344
+ }
3345
+ return `Connection to ${label} failed: ${detail}`;
3346
+ }
3347
+ async function validateSnowflakeConnectionForTest(connector, connection) {
3348
+ const warehouse = connection.warehouse?.trim();
3349
+ if (!warehouse) {
3350
+ return {
3351
+ ok: false,
3352
+ message: 'Snowflake connection requires a warehouse before it can be tested.',
3353
+ };
3354
+ }
3355
+ const warehouseRow = await findSnowflakeWarehouse(connector, warehouse);
3356
+ if (!warehouseRow) {
3357
+ return {
3358
+ ok: false,
3359
+ message: `Snowflake warehouse "${warehouse}" was not found or is not visible to this role.`,
3360
+ };
3361
+ }
3362
+ const state = String(readRowField(warehouseRow, 'state') ?? '').trim();
3363
+ const normalizedState = state.toUpperCase();
3364
+ if (normalizedState && normalizedState !== 'STARTED') {
3365
+ return {
3366
+ ok: false,
3367
+ message: `Snowflake warehouse "${warehouse}" is ${state}. Start or resume it, then test again.`,
3368
+ details: {
3369
+ warehouse,
3370
+ state,
3371
+ },
3372
+ };
3373
+ }
3374
+ const context = await connector.execute(`SELECT
3375
+ CURRENT_ACCOUNT() AS account_name,
3376
+ CURRENT_USER() AS user_name,
3377
+ CURRENT_ROLE() AS role_name,
3378
+ CURRENT_DATABASE() AS database_name,
3379
+ CURRENT_SCHEMA() AS schema_name,
3380
+ CURRENT_WAREHOUSE() AS warehouse_name`);
3381
+ const row = context.rows[0] ?? {};
3382
+ const user = String(readRowField(row, 'user_name') ?? connection.username ?? '').trim();
3383
+ const role = String(readRowField(row, 'role_name') ?? connection.role ?? '').trim();
3384
+ const activeWarehouse = String(readRowField(row, 'warehouse_name') ?? warehouse).trim();
3385
+ return {
3386
+ ok: true,
3387
+ message: `Connected to Snowflake${user ? ` as ${user}` : ''} using warehouse ${activeWarehouse || warehouse}.`,
3388
+ details: {
3389
+ warehouse: activeWarehouse || warehouse,
3390
+ warehouseState: state || 'STARTED',
3391
+ role: role || undefined,
3392
+ database: readRowField(row, 'database_name') ?? connection.database,
3393
+ schema: readRowField(row, 'schema_name') ?? connection.schema,
3394
+ },
3395
+ };
3396
+ }
3397
+ async function findSnowflakeWarehouse(connector, warehouse) {
3398
+ const candidates = Array.from(new Set([warehouse, warehouse.toUpperCase()]));
3399
+ for (const candidate of candidates) {
3400
+ const result = await connector.execute(`SHOW WAREHOUSES LIKE '${escapeSqlString(candidate)}'`);
3401
+ const row = result.rows.find((item) => {
3402
+ const name = String(readRowField(item, 'name') ?? '').trim();
3403
+ return name.localeCompare(warehouse, undefined, { sensitivity: 'accent' }) === 0;
3404
+ });
3405
+ if (row)
3406
+ return row;
3407
+ }
3408
+ return null;
3409
+ }
3410
+ function readRowField(row, field) {
3411
+ const expected = field.toLowerCase();
3412
+ const entry = Object.entries(row).find(([key]) => key.toLowerCase() === expected);
3413
+ return entry?.[1];
3414
+ }
3415
+ function escapeSqlString(value) {
3416
+ return value.replace(/'/g, "''");
3417
+ }
3418
+ function connectionDriverLabel(connection) {
3419
+ return connection.driver === 'snowflake' ? 'Snowflake' : connection.driver ?? 'database';
3420
+ }
3331
3421
  /**
3332
3422
  * Normalize connector QueryResult → SPA-friendly shape.
3333
3423
  * Connector returns columns as ColumnMeta[] ({name,type,driverType}).
@@ -3551,13 +3641,112 @@ function isPlaceholderLocalConnection(value) {
3551
3641
  }
3552
3642
  export function prepareLocalExecution(sql, connection, projectRoot, projectConfig) {
3553
3643
  const normalizedConnection = normalizeProjectConnection(connection, projectRoot);
3644
+ const dbtResolvedSql = resolveDbtMacrosForExecution(sql, projectRoot, projectConfig);
3554
3645
  return {
3555
3646
  sql: shouldResolveProjectPaths(normalizedConnection)
3556
- ? resolveProjectRelativeSqlPaths(sql, projectRoot, projectConfig.dataDir)
3557
- : sql,
3647
+ ? resolveProjectRelativeSqlPaths(dbtResolvedSql, projectRoot, projectConfig.dataDir)
3648
+ : dbtResolvedSql,
3558
3649
  connection: normalizedConnection,
3559
3650
  };
3560
3651
  }
3652
+ export function resolveDbtMacrosForExecution(sql, projectRoot, projectConfig = {}) {
3653
+ if (!/\{\{\s*(?:ref|source)\s*\(/i.test(sql))
3654
+ return sql;
3655
+ const manifestPath = resolveDbtManifestPath(projectRoot, projectConfig);
3656
+ if (!manifestPath) {
3657
+ throw new Error('dbt ref/source macros were found, but target/manifest.json was not available. Run dbt parse or dbt compile, then retry.');
3658
+ }
3659
+ const manifest = readJsonFile(manifestPath);
3660
+ const refs = buildDbtRelationLookup(manifest);
3661
+ const unresolved = new Set();
3662
+ let rendered = sql.replace(/\{\{\s*ref\(\s*(?:(['"])([^'"]+)\1\s*,\s*)?(['"])([^'"]+)\3(?:\s*,[^)]*)?\)\s*\}\}/gi, (match, _pkgQuote, packageName, _modelQuote, modelName) => {
3663
+ const key = normalizeDbtLookupKey(modelName);
3664
+ const scopedKey = packageName ? normalizeDbtLookupKey(`${packageName}.${modelName}`) : key;
3665
+ const relation = refs.models.get(scopedKey) ?? refs.models.get(key);
3666
+ if (!relation) {
3667
+ unresolved.add(packageName ? `ref('${packageName}', '${modelName}')` : `ref('${modelName}')`);
3668
+ return match;
3669
+ }
3670
+ return relation;
3671
+ });
3672
+ rendered = rendered.replace(/\{\{\s*source\(\s*(['"])([^'"]+)\1\s*,\s*(['"])([^'"]+)\3\s*\)\s*\}\}/gi, (match, _sourceQuote, sourceName, _tableQuote, tableName) => {
3673
+ const key = normalizeDbtLookupKey(`${sourceName}.${tableName}`);
3674
+ const relation = refs.sources.get(key) ?? refs.sources.get(normalizeDbtLookupKey(tableName));
3675
+ if (!relation) {
3676
+ unresolved.add(`source('${sourceName}', '${tableName}')`);
3677
+ return match;
3678
+ }
3679
+ return relation;
3680
+ });
3681
+ if (unresolved.size > 0) {
3682
+ throw new Error(`Could not resolve dbt macro${unresolved.size === 1 ? '' : 's'} from manifest.json: ${Array.from(unresolved).join(', ')}.`);
3683
+ }
3684
+ return rendered;
3685
+ }
3686
+ function buildDbtRelationLookup(manifest) {
3687
+ const models = new Map();
3688
+ const sources = new Map();
3689
+ const root = manifest && typeof manifest === 'object' ? manifest : {};
3690
+ const nodes = root.nodes && typeof root.nodes === 'object' ? root.nodes : {};
3691
+ const manifestSources = root.sources && typeof root.sources === 'object' ? root.sources : {};
3692
+ for (const [uniqueId, rawNode] of Object.entries(nodes)) {
3693
+ const node = rawNode && typeof rawNode === 'object' ? rawNode : null;
3694
+ if (!node || node.resource_type !== 'model')
3695
+ continue;
3696
+ const relation = dbtRelationName(node);
3697
+ if (!relation)
3698
+ continue;
3699
+ const name = stringField(node, 'name');
3700
+ const alias = stringField(node, 'alias');
3701
+ const packageName = uniqueId.split('.')[1];
3702
+ for (const key of [name, alias, packageName && name ? `${packageName}.${name}` : null, uniqueId]) {
3703
+ if (key)
3704
+ models.set(normalizeDbtLookupKey(key), relation);
3705
+ }
3706
+ }
3707
+ for (const [uniqueId, rawSource] of Object.entries(manifestSources)) {
3708
+ const source = rawSource && typeof rawSource === 'object' ? rawSource : null;
3709
+ if (!source)
3710
+ continue;
3711
+ const relation = dbtRelationName(source);
3712
+ if (!relation)
3713
+ continue;
3714
+ const sourceName = stringField(source, 'source_name');
3715
+ const name = stringField(source, 'name');
3716
+ const identifier = stringField(source, 'identifier');
3717
+ for (const key of [
3718
+ sourceName && name ? `${sourceName}.${name}` : null,
3719
+ sourceName && identifier ? `${sourceName}.${identifier}` : null,
3720
+ name,
3721
+ identifier,
3722
+ uniqueId,
3723
+ ]) {
3724
+ if (key)
3725
+ sources.set(normalizeDbtLookupKey(key), relation);
3726
+ }
3727
+ }
3728
+ return { models, sources };
3729
+ }
3730
+ function dbtRelationName(node) {
3731
+ const relationName = stringField(node, 'relation_name');
3732
+ if (relationName)
3733
+ return relationName;
3734
+ const database = stringField(node, 'database');
3735
+ const schema = stringField(node, 'schema');
3736
+ const alias = stringField(node, 'alias') ?? stringField(node, 'identifier') ?? stringField(node, 'name');
3737
+ if (database && schema && alias)
3738
+ return `${database}.${schema}.${alias}`;
3739
+ if (schema && alias)
3740
+ return `${schema}.${alias}`;
3741
+ return alias ?? null;
3742
+ }
3743
+ function stringField(source, key) {
3744
+ const value = source[key];
3745
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
3746
+ }
3747
+ function normalizeDbtLookupKey(value) {
3748
+ return value.trim().replace(/^['"]|['"]$/g, '').toLowerCase();
3749
+ }
3561
3750
  const AGENT_PREVIEW_FORBIDDEN_SQL = [
3562
3751
  'alter',
3563
3752
  'analyze',
@@ -4580,12 +4769,6 @@ function buildBlockStudioCertificationChecklist(input) {
4580
4769
  blockers.add(`${error.rule}: ${error.message}`);
4581
4770
  for (const blocker of input.extraBlockers ?? [])
4582
4771
  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
4772
  if (!input.previewSucceeded)
4590
4773
  blockers.add('Block has not run successfully');
4591
4774
  if (!input.testResults || input.testResults.failed > 0)
@@ -5280,7 +5463,7 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
5280
5463
  // Fall back to a live build.
5281
5464
  }
5282
5465
  }
5283
- const dbtManifestPath = resolveDbtManifestPath(projectRoot);
5466
+ const dbtManifestPath = resolveDbtManifestPath(projectRoot, {});
5284
5467
  try {
5285
5468
  const manifest = buildManifest({
5286
5469
  projectRoot,
@@ -5337,9 +5520,14 @@ function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
5337
5520
  return buildLineageGraph(blocks, metrics, dimensions);
5338
5521
  }
5339
5522
  }
5340
- function resolveDbtManifestPath(projectRoot) {
5341
- const candidate = join(projectRoot, 'target', 'manifest.json');
5342
- return existsSync(candidate) ? candidate : undefined;
5523
+ function resolveDbtManifestPath(projectRoot, projectConfig = {}) {
5524
+ const candidates = [];
5525
+ if (projectConfig.dbt?.projectDir || projectConfig.semanticLayer?.provider === 'dbt') {
5526
+ const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
5527
+ candidates.push(resolve(dbtProjectPath, projectConfig.dbt?.manifestPath ?? 'target/manifest.json'));
5528
+ }
5529
+ 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'));
5530
+ return candidates.find((candidate, index, list) => list.indexOf(candidate) === index && existsSync(candidate));
5343
5531
  }
5344
5532
  export function discoverDbtProfileConnections(projectRoot, projectConfig) {
5345
5533
  const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);