@duckcodeailabs/dql-cli 1.8.7 → 1.8.8
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/assets/dql-notebook/assets/{index-gPs-oUu4.js → index-DZ5M01EH.js} +241 -241
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +3 -0
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +2 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +8 -8
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +170 -78
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
package/dist/local-runtime.js
CHANGED
|
@@ -1186,7 +1186,8 @@ export async function startLocalServer(opts) {
|
|
|
1186
1186
|
? { preferredExecutionId: routeDecision.meaningResolution.recommendedExecutionId }
|
|
1187
1187
|
: {}),
|
|
1188
1188
|
executeCertifiedBlock: executeCertifiedBlockForAgent,
|
|
1189
|
-
executeGeneratedSql:
|
|
1189
|
+
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact),
|
|
1190
|
+
executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question),
|
|
1190
1191
|
getSchemaContext: getSchemaContextForAgent,
|
|
1191
1192
|
}, (turn) => {
|
|
1192
1193
|
if (turn.kind === 'thinking')
|
|
@@ -1296,51 +1297,53 @@ export async function startLocalServer(opts) {
|
|
|
1296
1297
|
},
|
|
1297
1298
|
];
|
|
1298
1299
|
}
|
|
1300
|
+
function generatedSqlArtifactContract(question, sql, seed) {
|
|
1301
|
+
const parameterized = parameterizeSqlForDqlImport(sql);
|
|
1302
|
+
const artifactName = seed?.name
|
|
1303
|
+
?? seed?.source.match(/\bblock\s+"([^"]+)"/i)?.[1]
|
|
1304
|
+
?? deriveGeneratedDraftSlug(question);
|
|
1305
|
+
const domain = seed?.source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1] ?? 'uncategorized';
|
|
1306
|
+
const source = candidateToDqlSource({
|
|
1307
|
+
name: artifactName,
|
|
1308
|
+
domain,
|
|
1309
|
+
description: question,
|
|
1310
|
+
owner: '',
|
|
1311
|
+
tags: ['ai-generated', 'review-required'],
|
|
1312
|
+
terms: [],
|
|
1313
|
+
pattern: '',
|
|
1314
|
+
grain: '',
|
|
1315
|
+
entities: [],
|
|
1316
|
+
outputs: [],
|
|
1317
|
+
dimensions: [],
|
|
1318
|
+
allowedFilters: parameterized.allowedFilters,
|
|
1319
|
+
parameterPolicy: parameterized.parameterPolicy,
|
|
1320
|
+
filterBindings: parameterized.filterBindings,
|
|
1321
|
+
parameterDecisions: parameterized.parameterDecisions,
|
|
1322
|
+
sourceSystems: [],
|
|
1323
|
+
replacementFor: [],
|
|
1324
|
+
reviewCadence: 'monthly',
|
|
1325
|
+
sql: parameterized.sql,
|
|
1326
|
+
llmContext: `Review-required DQL generated for: ${question}`,
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
...(seed ?? { kind: 'sql_block' }),
|
|
1330
|
+
kind: 'sql_block',
|
|
1331
|
+
name: artifactName,
|
|
1332
|
+
source,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1299
1335
|
/**
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1304
|
-
* model call.
|
|
1336
|
+
* AGT-010 / API-003 / UI-011 — attach the exact artifact already used for
|
|
1337
|
+
* execution. Post-execution translation is forbidden because it creates a
|
|
1338
|
+
* second query contract whose Apply result can differ from the displayed Ask
|
|
1339
|
+
* result.
|
|
1305
1340
|
*/
|
|
1306
1341
|
function attachExecutableDqlArtifactContract(governedAnswer, question) {
|
|
1307
1342
|
const originalSql = governedAnswer.proposedSql?.trim() || governedAnswer.sql?.trim();
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
?? artifact?.source.match(/\bblock\s+"([^"]+)"/i)?.[1]
|
|
1313
|
-
?? deriveGeneratedDraftSlug(question);
|
|
1314
|
-
const domain = artifact?.source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1] ?? 'uncategorized';
|
|
1315
|
-
const source = candidateToDqlSource({
|
|
1316
|
-
name: artifactName,
|
|
1317
|
-
domain,
|
|
1318
|
-
description: question,
|
|
1319
|
-
owner: '',
|
|
1320
|
-
tags: ['ai-generated', 'review-required'],
|
|
1321
|
-
terms: [],
|
|
1322
|
-
pattern: '',
|
|
1323
|
-
grain: '',
|
|
1324
|
-
entities: [],
|
|
1325
|
-
outputs: [],
|
|
1326
|
-
dimensions: [],
|
|
1327
|
-
allowedFilters: parameterized.allowedFilters,
|
|
1328
|
-
parameterPolicy: parameterized.parameterPolicy,
|
|
1329
|
-
filterBindings: parameterized.filterBindings,
|
|
1330
|
-
parameterDecisions: parameterized.parameterDecisions,
|
|
1331
|
-
sourceSystems: [],
|
|
1332
|
-
replacementFor: [],
|
|
1333
|
-
reviewCadence: 'monthly',
|
|
1334
|
-
sql: parameterized.sql,
|
|
1335
|
-
llmContext: `Review-required DQL generated for: ${question}`,
|
|
1336
|
-
});
|
|
1337
|
-
artifact = {
|
|
1338
|
-
...(artifact ?? { kind: 'sql_block' }),
|
|
1339
|
-
kind: 'sql_block',
|
|
1340
|
-
name: artifactName,
|
|
1341
|
-
source,
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1343
|
+
const executedArtifact = normalizeDqlArtifactReference(governedAnswer.result?.dqlArtifact);
|
|
1344
|
+
let artifact = executedArtifact ?? governedAnswer.dqlArtifact;
|
|
1345
|
+
if (!artifact && originalSql)
|
|
1346
|
+
artifact = generatedSqlArtifactContract(question, originalSql);
|
|
1344
1347
|
if (!artifact?.source)
|
|
1345
1348
|
return;
|
|
1346
1349
|
const invocation = prepareBlockInvocation({
|
|
@@ -1357,8 +1360,11 @@ export async function startLocalServer(opts) {
|
|
|
1357
1360
|
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
1358
1361
|
persistence: artifact.sourcePath ? 'saved' : 'transient',
|
|
1359
1362
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
1360
|
-
...(originalSql ? { compiledSql: originalSql } : {}),
|
|
1363
|
+
...(governedAnswer.result?.sql ? { compiledSql: governedAnswer.result.sql } : originalSql ? { compiledSql: originalSql } : {}),
|
|
1364
|
+
...(governedAnswer.result?.executionReceipt ? { executionReceipt: governedAnswer.result.executionReceipt } : {}),
|
|
1361
1365
|
};
|
|
1366
|
+
if (governedAnswer.result)
|
|
1367
|
+
delete governedAnswer.result.dqlArtifact;
|
|
1362
1368
|
}
|
|
1363
1369
|
const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
|
|
1364
1370
|
let governedAnswer;
|
|
@@ -2593,22 +2599,27 @@ export async function startLocalServer(opts) {
|
|
|
2593
2599
|
throw new Error(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`);
|
|
2594
2600
|
}
|
|
2595
2601
|
const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, activeConnection, projectRoot, projectConfig);
|
|
2602
|
+
const executionSql = invocationInput?.rowLimit
|
|
2603
|
+
? buildAgentPreviewSql(prepared.sql, invocationInput.rowLimit)
|
|
2604
|
+
: prepared.sql;
|
|
2596
2605
|
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
2597
2606
|
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
2598
2607
|
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
2599
|
-
const rawResult = await executor.executeQuery(
|
|
2608
|
+
const rawResult = await executor.executeQuery(executionSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
|
|
2600
2609
|
const normalized = normalizeQueryResult(rawResult);
|
|
2610
|
+
const executionReceipt = createDqlArtifactExecutionReceipt(source, executionSql, invocation.values, normalized);
|
|
2601
2611
|
return {
|
|
2602
2612
|
columns: normalized.columns,
|
|
2603
2613
|
rows: normalized.rows,
|
|
2604
2614
|
rowCount: normalized.rowCount,
|
|
2605
2615
|
executionTime: normalized.executionTime,
|
|
2606
2616
|
chartConfig: plan?.chartConfig ?? (metadata.chartType ? { chart: metadata.chartType } : undefined),
|
|
2607
|
-
sql:
|
|
2617
|
+
sql: executionSql,
|
|
2608
2618
|
...(metadata.name ? { blockName: metadata.name } : {}),
|
|
2609
2619
|
...(metadata.path ? { blockPath: metadata.path } : {}),
|
|
2610
2620
|
parameters: invocation.resolvedParameters,
|
|
2611
2621
|
auditId: invocation.auditId,
|
|
2622
|
+
executionReceipt,
|
|
2612
2623
|
};
|
|
2613
2624
|
};
|
|
2614
2625
|
const executeCertifiedBlockByNameForAgent = async (blockName, invocationInput, requireCertified = false) => {
|
|
@@ -2621,12 +2632,29 @@ export async function startLocalServer(opts) {
|
|
|
2621
2632
|
throw new Error(`Block "${block.name}" is not certified and cannot be rerun as a certified answer.`);
|
|
2622
2633
|
}
|
|
2623
2634
|
const source = readFileSync(join(projectRoot, block.filePath), 'utf-8');
|
|
2624
|
-
|
|
2635
|
+
const result = await executeDqlArtifactSourceForAgent(source, {
|
|
2625
2636
|
name: block.name,
|
|
2626
2637
|
path: block.filePath,
|
|
2627
2638
|
domain: block.domain,
|
|
2628
2639
|
chartType: block.chartType,
|
|
2629
2640
|
}, invocationInput);
|
|
2641
|
+
return {
|
|
2642
|
+
...result,
|
|
2643
|
+
dqlArtifact: {
|
|
2644
|
+
kind: 'certified_block',
|
|
2645
|
+
name: block.name,
|
|
2646
|
+
source,
|
|
2647
|
+
sourcePath: block.filePath,
|
|
2648
|
+
persistence: 'saved',
|
|
2649
|
+
trustState: 'certified',
|
|
2650
|
+
compiledSql: result.sql,
|
|
2651
|
+
executionReceipt: result.executionReceipt,
|
|
2652
|
+
...(invocationInput?.rowLimit ? { limit: invocationInput.rowLimit } : {}),
|
|
2653
|
+
...(invocationInput?.parameters && Object.keys(invocationInput.parameters).length > 0
|
|
2654
|
+
? { parameterValues: invocationInput.parameters }
|
|
2655
|
+
: {}),
|
|
2656
|
+
},
|
|
2657
|
+
};
|
|
2630
2658
|
};
|
|
2631
2659
|
const executeCertifiedBlockForAgent = async (node, invocationInput) => {
|
|
2632
2660
|
if (node.kind !== 'block') {
|
|
@@ -2634,26 +2662,41 @@ export async function startLocalServer(opts) {
|
|
|
2634
2662
|
}
|
|
2635
2663
|
return executeCertifiedBlockByNameForAgent(node.name || node.nodeId.replace(/^block:/, ''), invocationInput);
|
|
2636
2664
|
};
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
const
|
|
2665
|
+
// Secondary agent surfaces use this compatibility callback. Delegate to the
|
|
2666
|
+
// same artifact-first path as Ask so research/app/notebook execution cannot
|
|
2667
|
+
// silently reintroduce a raw-SQL result followed by a different DQL artifact.
|
|
2668
|
+
const executeGeneratedSqlForAgent = async (sql, artifact) => executeGeneratedArtifactForAgent(artifact?.name ?? 'AI-generated query', sql, artifact);
|
|
2669
|
+
const executeArtifactReferenceForAgent = async (artifact, question) => {
|
|
2670
|
+
const invocation = prepareBlockInvocation({
|
|
2671
|
+
source: artifact.source,
|
|
2672
|
+
parameters: artifact.parameterValues,
|
|
2673
|
+
question,
|
|
2674
|
+
surface: 'ask_ai',
|
|
2675
|
+
});
|
|
2676
|
+
const result = await executeDqlArtifactSourceForAgent(artifact.source, {
|
|
2677
|
+
name: artifact.name,
|
|
2678
|
+
path: artifact.sourcePath,
|
|
2679
|
+
domain: artifact.source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1],
|
|
2680
|
+
}, {
|
|
2681
|
+
question,
|
|
2682
|
+
parameters: invocation.values,
|
|
2683
|
+
rowLimit: artifact.limit ?? 200,
|
|
2684
|
+
});
|
|
2649
2685
|
return {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2686
|
+
...result,
|
|
2687
|
+
dqlArtifact: {
|
|
2688
|
+
...artifact,
|
|
2689
|
+
...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
|
|
2690
|
+
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
2691
|
+
compiledSql: result.sql,
|
|
2692
|
+
executionReceipt: result.executionReceipt,
|
|
2693
|
+
},
|
|
2655
2694
|
};
|
|
2656
2695
|
};
|
|
2696
|
+
const executeGeneratedArtifactForAgent = async (question, sql, seed) => {
|
|
2697
|
+
const artifact = generatedSqlArtifactContract(question, sql, seed);
|
|
2698
|
+
return executeArtifactReferenceForAgent({ ...artifact, limit: seed?.limit ?? artifact.limit ?? 200 }, question);
|
|
2699
|
+
};
|
|
2657
2700
|
/**
|
|
2658
2701
|
* EXP-001: execution host for the deliberately narrow non-governed lane.
|
|
2659
2702
|
* It does not reinterpret dbt lineage or shared names as relationship proof.
|
|
@@ -2681,7 +2724,7 @@ export async function startLocalServer(opts) {
|
|
|
2681
2724
|
if (!analysis.parsed) {
|
|
2682
2725
|
return { proofs: [], sql: boundedSql, repairs, error: 'DQL could not parse the exploratory SQL to validate its join predicates.' };
|
|
2683
2726
|
}
|
|
2684
|
-
const probeableJoins = probeableExploratoryJoins(analysis.joins, analysis.ctes);
|
|
2727
|
+
const probeableJoins = probeableExploratoryJoins(analysis.joins, analysis.ctes, analysis.derivedRelations);
|
|
2685
2728
|
// A normal customer → order → order-item → product question needs three
|
|
2686
2729
|
// joins. Keep the lane bounded, but do not reject this common dbt-star path
|
|
2687
2730
|
// solely because the old two-join demo limit was too small. CTE-internal
|
|
@@ -2754,7 +2797,7 @@ export async function startLocalServer(opts) {
|
|
|
2754
2797
|
return { proofs, sql: boundedSql, repairs, error: contradiction };
|
|
2755
2798
|
}
|
|
2756
2799
|
}
|
|
2757
|
-
const result = await
|
|
2800
|
+
const result = await executeGeneratedArtifactForAgent(question, boundedSql);
|
|
2758
2801
|
return { result, proofs, sql: boundedSql, repairs };
|
|
2759
2802
|
}
|
|
2760
2803
|
catch (error) {
|
|
@@ -10520,13 +10563,17 @@ export async function startLocalServer(opts) {
|
|
|
10520
10563
|
domain = block.domain;
|
|
10521
10564
|
chartType = block.chartType;
|
|
10522
10565
|
}
|
|
10523
|
-
else if (sourcePath) {
|
|
10566
|
+
else if (!source.trim() && sourcePath) {
|
|
10524
10567
|
const filePath = safeJoin(projectRoot, sourcePath);
|
|
10525
10568
|
if (filePath && existsSync(filePath) && !statSync(filePath).isDirectory())
|
|
10526
10569
|
source = readFileSync(filePath, 'utf-8');
|
|
10527
10570
|
}
|
|
10528
10571
|
if (!source.trim())
|
|
10529
10572
|
throw new Error('Provide an executable DQL artifact or block name.');
|
|
10573
|
+
const expectedReceipt = requestedArtifact?.executionReceipt;
|
|
10574
|
+
if (expectedReceipt && expectedReceipt.sourceFingerprint !== executionFingerprint(source)) {
|
|
10575
|
+
throw new Error('DQL artifact source changed after the answer was produced. Refresh the answer before applying it.');
|
|
10576
|
+
}
|
|
10530
10577
|
const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
10531
10578
|
? body.parameters
|
|
10532
10579
|
: requestedArtifact?.parameterValues ?? {};
|
|
@@ -10538,7 +10585,15 @@ export async function startLocalServer(opts) {
|
|
|
10538
10585
|
}, {
|
|
10539
10586
|
parameters,
|
|
10540
10587
|
question: typeof body.question === 'string' ? body.question : undefined,
|
|
10588
|
+
rowLimit: requestedArtifact?.limit,
|
|
10541
10589
|
});
|
|
10590
|
+
const actualReceipt = result.executionReceipt;
|
|
10591
|
+
if (expectedReceipt && actualReceipt) {
|
|
10592
|
+
if (expectedReceipt.parameterFingerprint === actualReceipt.parameterFingerprint
|
|
10593
|
+
&& expectedReceipt.compiledSqlFingerprint !== actualReceipt.compiledSqlFingerprint) {
|
|
10594
|
+
throw new Error('DQL artifact execution changed while its source and inputs were unchanged. Refresh the answer before applying it.');
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10542
10597
|
const contract = prepareBlockInvocation({
|
|
10543
10598
|
source,
|
|
10544
10599
|
parameters,
|
|
@@ -10560,6 +10615,8 @@ export async function startLocalServer(opts) {
|
|
|
10560
10615
|
persistence: sourcePath ? 'saved' : 'transient',
|
|
10561
10616
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
10562
10617
|
compiledSql: result.sql,
|
|
10618
|
+
executionReceipt: result.executionReceipt,
|
|
10619
|
+
...(requestedArtifact?.limit ? { limit: requestedArtifact.limit } : {}),
|
|
10563
10620
|
},
|
|
10564
10621
|
}));
|
|
10565
10622
|
}
|
|
@@ -12424,6 +12481,37 @@ function titleFromSlug(value) {
|
|
|
12424
12481
|
function stableShortHash(value) {
|
|
12425
12482
|
return createHash('sha1').update(value).digest('hex').slice(0, 10);
|
|
12426
12483
|
}
|
|
12484
|
+
function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, result) {
|
|
12485
|
+
return {
|
|
12486
|
+
sourceFingerprint: executionFingerprint(source),
|
|
12487
|
+
compiledSqlFingerprint: executionFingerprint(compiledSql),
|
|
12488
|
+
parameterFingerprint: executionFingerprint(stableExecutionValue(parameters)),
|
|
12489
|
+
resultFingerprint: executionFingerprint(stableExecutionValue({
|
|
12490
|
+
columns: result.columns,
|
|
12491
|
+
rows: result.rows,
|
|
12492
|
+
rowCount: result.rowCount,
|
|
12493
|
+
})),
|
|
12494
|
+
};
|
|
12495
|
+
}
|
|
12496
|
+
function executionFingerprint(value) {
|
|
12497
|
+
return createHash('sha256').update(value).digest('hex');
|
|
12498
|
+
}
|
|
12499
|
+
function stableExecutionValue(value) {
|
|
12500
|
+
const seen = new WeakSet();
|
|
12501
|
+
return JSON.stringify(value, (_key, item) => {
|
|
12502
|
+
if (!item || typeof item !== 'object') {
|
|
12503
|
+
if (typeof item === 'bigint')
|
|
12504
|
+
return item.toString();
|
|
12505
|
+
return item;
|
|
12506
|
+
}
|
|
12507
|
+
if (seen.has(item))
|
|
12508
|
+
return '[Circular]';
|
|
12509
|
+
seen.add(item);
|
|
12510
|
+
if (Array.isArray(item))
|
|
12511
|
+
return item;
|
|
12512
|
+
return Object.fromEntries(Object.entries(item).sort(([left], [right]) => left.localeCompare(right)));
|
|
12513
|
+
});
|
|
12514
|
+
}
|
|
12427
12515
|
function renderCompostingPrBody(candidates) {
|
|
12428
12516
|
const lines = [
|
|
12429
12517
|
'# Semantic Composting Changeset',
|
|
@@ -13113,15 +13201,18 @@ const AGENT_PREVIEW_FORBIDDEN_SQL = [
|
|
|
13113
13201
|
'update',
|
|
13114
13202
|
'vacuum',
|
|
13115
13203
|
];
|
|
13116
|
-
export function buildAgentPreviewSql(sql) {
|
|
13204
|
+
export function buildAgentPreviewSql(sql, rowLimit = 200) {
|
|
13117
13205
|
const trimmed = sql.trim();
|
|
13118
13206
|
if (!trimmed)
|
|
13119
13207
|
throw new Error('Generated SQL preview is empty.');
|
|
13208
|
+
const boundedRowLimit = Number.isFinite(rowLimit)
|
|
13209
|
+
? Math.max(1, Math.min(10_000, Math.floor(rowLimit)))
|
|
13210
|
+
: 200;
|
|
13120
13211
|
const withoutTrailingSemicolon = trimmed.replace(/;\s*$/, '').trim();
|
|
13121
13212
|
const readOnlyError = readOnlySqlValidationError(withoutTrailingSemicolon, 'Generated SQL preview');
|
|
13122
13213
|
if (readOnlyError)
|
|
13123
13214
|
throw new Error(readOnlyError);
|
|
13124
|
-
return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT
|
|
13215
|
+
return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT ${boundedRowLimit}`;
|
|
13125
13216
|
}
|
|
13126
13217
|
/**
|
|
13127
13218
|
* EXP-001 preflight for model-authored exploratory SQL.
|
|
@@ -13421,20 +13512,21 @@ SELECT
|
|
|
13421
13512
|
(SELECT COALESCE(MAX(key_count), 0) FROM right_key_counts) AS max_right_rows_per_key`;
|
|
13422
13513
|
}
|
|
13423
13514
|
/**
|
|
13424
|
-
* CTE names are query-internal derived relations, not
|
|
13425
|
-
*
|
|
13426
|
-
* be excluded from declared-path enforcement and
|
|
13427
|
-
* `FROM "
|
|
13428
|
-
*
|
|
13429
|
-
* the parsed join list and
|
|
13515
|
+
* CTE names and nested SELECT aliases are query-internal derived relations, not
|
|
13516
|
+
* warehouse tables. A join whose endpoint is `joy_items` or generated
|
|
13517
|
+
* `subq_2` must be excluded from declared-path enforcement and join probes —
|
|
13518
|
+
* probing it as `FROM "subq_2"` asks Snowflake for an object that only exists
|
|
13519
|
+
* inside the statement. Physical joins inside the derived relation remain in
|
|
13520
|
+
* the parsed join list and are validated/probed on their own.
|
|
13430
13521
|
*/
|
|
13431
|
-
export function probeableExploratoryJoins(joins, ctes) {
|
|
13432
|
-
const
|
|
13433
|
-
|
|
13522
|
+
export function probeableExploratoryJoins(joins, ctes, derivedRelations = []) {
|
|
13523
|
+
const internalNames = new Set([...ctes, ...derivedRelations]
|
|
13524
|
+
.map((name) => name.replace(/["`\[\]]/g, '').split('.').at(-1)?.toLowerCase() ?? name.toLowerCase()));
|
|
13525
|
+
if (internalNames.size === 0)
|
|
13434
13526
|
return joins;
|
|
13435
13527
|
return joins.filter((join) => ![join.leftRelation, join.rightRelation].some((relation) => {
|
|
13436
13528
|
const bare = relation?.replace(/["`\[\]]/g, '').split('.').at(-1)?.toLowerCase();
|
|
13437
|
-
return Boolean(bare &&
|
|
13529
|
+
return Boolean(bare && internalNames.has(bare));
|
|
13438
13530
|
}));
|
|
13439
13531
|
}
|
|
13440
13532
|
function declaredExploratoryRelationMatches(sqlRelation, declaredRelation) {
|