@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.
- package/dist/apps-api.d.ts +19 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +279 -9
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/index-L-zyCapt.js +3636 -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/import.js +6 -6
- package/dist/commands/import.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/local-runtime.d.ts +8 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +217 -29
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +11 -11
- package/dist/assets/dql-notebook/assets/index-BFUUTIWF.js +0 -3618
package/dist/local-runtime.js
CHANGED
|
@@ -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
|
|
404
|
-
const result = await executor.executeQuery(sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}),
|
|
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:
|
|
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
|
-
|
|
2942
|
+
target = normalizeProjectConnection(isConnectionConfig(body.connection) ? body.connection : connection, projectRoot);
|
|
2945
2943
|
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
|
-
}));
|
|
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(
|
|
2949
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
2956
2950
|
res.end(serializeJSON({
|
|
2957
2951
|
ok: false,
|
|
2958
|
-
message:
|
|
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(
|
|
3557
|
-
:
|
|
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
|
|
5342
|
-
|
|
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);
|