@duckcodeailabs/dql-cli 1.8.2 → 1.8.4
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-DGbXmDU6.js → index-CPG4A872.js} +340 -340
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-invocation.d.ts +2 -2
- package/dist/block-invocation.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +62 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/dbt-cloud-semantic.d.ts +42 -0
- package/dist/dbt-cloud-semantic.d.ts.map +1 -0
- package/dist/dbt-cloud-semantic.js +185 -0
- package/dist/dbt-cloud-semantic.js.map +1 -0
- package/dist/llm/analytics-tools.d.ts.map +1 -1
- package/dist/llm/analytics-tools.js +13 -1
- package/dist/llm/analytics-tools.js.map +1 -1
- package/dist/llm/cancellation.d.ts +13 -0
- package/dist/llm/cancellation.d.ts.map +1 -0
- package/dist/llm/cancellation.js +19 -0
- package/dist/llm/cancellation.js.map +1 -0
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +6 -0
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +3 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +40 -3
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +547 -217
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/providers/subscription-cli.d.ts.map +1 -1
- package/dist/providers/subscription-cli.js +16 -0
- package/dist/providers/subscription-cli.js.map +1 -1
- package/dist/retrieval-health.d.ts +29 -0
- package/dist/retrieval-health.d.ts.map +1 -0
- package/dist/retrieval-health.js +88 -0
- package/dist/retrieval-health.js.map +1 -0
- package/dist/semantic-runtime-settings.d.ts +55 -0
- package/dist/semantic-runtime-settings.d.ts.map +1 -0
- package/dist/semantic-runtime-settings.js +191 -0
- package/dist/semantic-runtime-settings.js.map +1 -0
- package/dist/semantic-runtime.d.ts +80 -0
- package/dist/semantic-runtime.d.ts.map +1 -0
- package/dist/semantic-runtime.js +238 -0
- package/dist/semantic-runtime.js.map +1 -0
- package/dist/version-status.d.ts +28 -0
- package/dist/version-status.d.ts.map +1 -0
- package/dist/version-status.js +118 -0
- package/dist/version-status.js.map +1 -0
- package/package.json +10 -10
package/dist/local-runtime.js
CHANGED
|
@@ -14,9 +14,12 @@ import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetri
|
|
|
14
14
|
import { load as loadYaml } from 'js-yaml';
|
|
15
15
|
import { listBlockTemplates } from './block-templates.js';
|
|
16
16
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
17
|
+
import { rethrowIfCancelled } from './llm/cancellation.js';
|
|
18
|
+
import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from './version-status.js';
|
|
19
|
+
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
17
20
|
import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
18
21
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
19
|
-
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, buildLocalContextPack, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine,
|
|
22
|
+
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, buildLocalContextPack, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
|
|
20
23
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
21
24
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
|
|
22
25
|
import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
@@ -28,7 +31,9 @@ import { LocalAppStorage, LocalNotebookResearchStorage, defaultLocalAppsDbPath,
|
|
|
28
31
|
import { Certifier, ENTERPRISE_RULES, evaluateInvariants, hasInvariantViolation, } from '@duckcodeailabs/dql-governance';
|
|
29
32
|
import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
|
|
30
33
|
import { clearBlockStudioImportSessions, candidateToDqlSource, createBlockStudioImportSession, deleteBlockStudioImportSession, listBlockStudioImportSessions, loadBlockStudioImportSession, readBlockStudioImportCandidate, parameterizeSqlForDqlImport, updateBlockStudioImportCandidate, writeBlockStudioImportSession, writeBlockStudioImportCandidate, } from './block-studio-import.js';
|
|
31
|
-
import { MetricFlowUnavailableError,
|
|
34
|
+
import { MetricFlowUnavailableError, hasDbtSemanticManifest, } from "./metricflow.js";
|
|
35
|
+
import { compileSemanticRuntimeQuery, getSemanticRuntimeStatus, isSemanticRuntimeError, listRuntimeCompatibleDimensions, semanticMetricExecutionCapability as runtimeMetricExecutionCapability, SemanticRuntimeRequiredError, testSemanticRuntimeDraft, } from './semantic-runtime.js';
|
|
36
|
+
import { getSemanticRuntimeSettings, saveTestedSemanticRuntimeSettings, } from './semantic-runtime-settings.js';
|
|
32
37
|
import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
|
|
33
38
|
import { prepareBlockInvocation } from './block-invocation.js';
|
|
34
39
|
const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
|
|
@@ -164,17 +169,26 @@ export function parseAgentRunRequestBody(body) {
|
|
|
164
169
|
}
|
|
165
170
|
const AGENT_LOOKUP_DEADLINE_MS = 45_000;
|
|
166
171
|
const AGENT_RESEARCH_DEADLINE_MS = 120_000;
|
|
172
|
+
/** Env-tunable run deadline (e.g. slow subscription-CLI providers), clamped to sane bounds. */
|
|
173
|
+
function resolveAgentDeadlineMs(envKey, fallback, env = process.env) {
|
|
174
|
+
const configured = Number(env[envKey]);
|
|
175
|
+
if (!Number.isFinite(configured) || configured <= 0)
|
|
176
|
+
return fallback;
|
|
177
|
+
return Math.max(15_000, Math.min(600_000, Math.floor(configured)));
|
|
178
|
+
}
|
|
167
179
|
/**
|
|
168
180
|
* PERF-002: one wall-clock budget follows the request through routing, provider
|
|
169
181
|
* calls, repair, and execution. Ordinary Ask never inherits Research's budget
|
|
170
182
|
* merely because it spans two tables; explicit/deep investigation does.
|
|
171
183
|
*/
|
|
172
|
-
export function agentRunDeadlineMs(request) {
|
|
184
|
+
export function agentRunDeadlineMs(request, env = process.env) {
|
|
185
|
+
const lookupDeadline = resolveAgentDeadlineMs('DQL_AGENT_LOOKUP_DEADLINE_MS', AGENT_LOOKUP_DEADLINE_MS, env);
|
|
186
|
+
const researchDeadline = resolveAgentDeadlineMs('DQL_AGENT_RESEARCH_DEADLINE_MS', AGENT_RESEARCH_DEADLINE_MS, env);
|
|
173
187
|
if (request.requestedMode === 'research' || request.analysisDepth === 'deep') {
|
|
174
|
-
return
|
|
188
|
+
return researchDeadline;
|
|
175
189
|
}
|
|
176
190
|
const plan = buildAnalysisQuestionPlan(request.question);
|
|
177
|
-
return plan.needsResearchWorkspace ?
|
|
191
|
+
return plan.needsResearchWorkspace ? researchDeadline : lookupDeadline;
|
|
178
192
|
}
|
|
179
193
|
export function shouldSynthesizeAgentRunAnswer(governedAnswer) {
|
|
180
194
|
if (governedAnswer.kind === 'no_answer')
|
|
@@ -215,7 +229,11 @@ function businessNarrativeGaps(warnings) {
|
|
|
215
229
|
// source of prior turns (survives refresh); the client-built context remains
|
|
216
230
|
// the fallback for embedders that never send a threadId.
|
|
217
231
|
async function conversationContextFromThread(store, threadId, clientContext, question) {
|
|
218
|
-
|
|
232
|
+
// Token-budget backstop: six verbatim turns with per-field caps. Older turns
|
|
233
|
+
// remain reachable through the rolling summary + semantic recall below —
|
|
234
|
+
// carrying more raw prose mostly slows every provider call on follow-ups.
|
|
235
|
+
const clampTurnText = (value) => value && value.length > 1_200 ? `${value.slice(0, 1_200)}…` : value;
|
|
236
|
+
const turns = store.recentTurns(threadId, 6).map((turn) => {
|
|
219
237
|
const contract = turn.contract ?? {};
|
|
220
238
|
const topN = contract.topN;
|
|
221
239
|
const topNValue = typeof topN === 'number'
|
|
@@ -225,8 +243,8 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
225
243
|
: undefined;
|
|
226
244
|
return compactConversationRecord({
|
|
227
245
|
id: turn.id,
|
|
228
|
-
question: turn.question,
|
|
229
|
-
answerSummary: turn.answerSummary,
|
|
246
|
+
question: clampTurnText(turn.question),
|
|
247
|
+
answerSummary: clampTurnText(turn.answerSummary),
|
|
230
248
|
sourceCertifiedBlock: turn.sourceCertifiedBlock,
|
|
231
249
|
route: turn.route,
|
|
232
250
|
trustLabel: turn.trustLabel,
|
|
@@ -425,6 +443,8 @@ export async function startLocalServer(opts) {
|
|
|
425
443
|
const loopback = bindHost === '127.0.0.1' || bindHost === 'localhost' || bindHost === '::1';
|
|
426
444
|
const authToken = opts.authToken ?? process.env.DQL_SERVER_TOKEN;
|
|
427
445
|
const runtimeVersion = readDqlRuntimeVersion();
|
|
446
|
+
// Warm the latest-version cache in the background (2s cap, 24h cache; offline → unknown).
|
|
447
|
+
void fetchLatestPublishedDqlVersion();
|
|
428
448
|
const allowedOrigins = new Set((opts.allowedOrigins ?? (process.env.DQL_ALLOWED_ORIGINS ?? '').split(','))
|
|
429
449
|
.map((value) => value.trim().replace(/\/$/, ''))
|
|
430
450
|
.filter(Boolean));
|
|
@@ -1069,12 +1089,17 @@ export async function startLocalServer(opts) {
|
|
|
1069
1089
|
// dialect-correct SQL (e.g. DATE_TRUNC / identifier quoting). Absent when no
|
|
1070
1090
|
// connection is configured — the compiler then uses its default dialect.
|
|
1071
1091
|
let semanticDriver;
|
|
1092
|
+
let semanticConnection;
|
|
1072
1093
|
try {
|
|
1073
|
-
|
|
1094
|
+
semanticConnection = requireActiveConnection();
|
|
1095
|
+
semanticDriver = semanticConnection.driver;
|
|
1074
1096
|
}
|
|
1075
1097
|
catch {
|
|
1076
1098
|
semanticDriver = undefined;
|
|
1077
1099
|
}
|
|
1100
|
+
const semanticTableMapping = semanticLayer && semanticConnection
|
|
1101
|
+
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer)
|
|
1102
|
+
: undefined;
|
|
1078
1103
|
const requestedDomain = agentRunWorkspaceValue(request, 'domain');
|
|
1079
1104
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
1080
1105
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
@@ -1093,7 +1118,12 @@ export async function startLocalServer(opts) {
|
|
|
1093
1118
|
await runner.run({
|
|
1094
1119
|
provider: resolvedProvider,
|
|
1095
1120
|
messages: [
|
|
1096
|
-
|
|
1121
|
+
// No-thread fallback path: bound the raw client history so follow-ups
|
|
1122
|
+
// cannot inflate every provider call (8 turns × 4k chars).
|
|
1123
|
+
...(request.history ?? []).slice(-8).map((message) => ({
|
|
1124
|
+
role: message.role,
|
|
1125
|
+
content: message.text.length > 4_000 ? `${message.text.slice(0, 4_000)}…` : message.text,
|
|
1126
|
+
})),
|
|
1097
1127
|
...(isRepair
|
|
1098
1128
|
? [{ role: 'assistant', content: `The prior attempt needs repair without changing the original question or requested output grain: ${repair?.repairHint}` }]
|
|
1099
1129
|
: []),
|
|
@@ -1116,6 +1146,26 @@ export async function startLocalServer(opts) {
|
|
|
1116
1146
|
projectSnapshots.assertCurrent(snapshotId);
|
|
1117
1147
|
},
|
|
1118
1148
|
...(semanticDriver ? { semanticDriver } : {}),
|
|
1149
|
+
...(semanticTableMapping ? { semanticTableMapping } : {}),
|
|
1150
|
+
...(semanticLayer ? {
|
|
1151
|
+
semanticQueryCompiler: async (selection) => {
|
|
1152
|
+
const compiled = await compileSemanticRuntimeQuery({
|
|
1153
|
+
...selection,
|
|
1154
|
+
dimensions: selection.dimensions ?? [],
|
|
1155
|
+
}, {
|
|
1156
|
+
projectRoot,
|
|
1157
|
+
projectConfig,
|
|
1158
|
+
detectedProvider: semanticDetectedProvider,
|
|
1159
|
+
semanticLayer: semanticLayer,
|
|
1160
|
+
driver: semanticDriver,
|
|
1161
|
+
tableMapping: semanticTableMapping,
|
|
1162
|
+
});
|
|
1163
|
+
if (!compiled) {
|
|
1164
|
+
throw new SemanticRuntimeRequiredError('The selected semantic members could not be composed. Configure dbt Cloud Semantic Layer or a compatible local MetricFlow runtime for derived metrics.');
|
|
1165
|
+
}
|
|
1166
|
+
return { sql: compiled.sql, engine: compiled.engine };
|
|
1167
|
+
},
|
|
1168
|
+
} : {}),
|
|
1119
1169
|
...(routeDecision?.meaningResolution?.selectedConceptIds.length
|
|
1120
1170
|
? { preferredEvidenceIds: routeDecision.meaningResolution.selectedConceptIds }
|
|
1121
1171
|
: {}),
|
|
@@ -1429,6 +1479,9 @@ export async function startLocalServer(opts) {
|
|
|
1429
1479
|
applySmartVisualization(governedAnswer, request.question);
|
|
1430
1480
|
}
|
|
1431
1481
|
catch (error) {
|
|
1482
|
+
// Deadline/cancellation propagates to the engine, which renders the
|
|
1483
|
+
// graceful "bounded execution deadline" outcome — never a provider error.
|
|
1484
|
+
rethrowIfCancelled(error, request.signal);
|
|
1432
1485
|
const message = formatAgentRunInfrastructureError(error, 'AI answer provider');
|
|
1433
1486
|
return {
|
|
1434
1487
|
summary: message,
|
|
@@ -1456,7 +1509,7 @@ export async function startLocalServer(opts) {
|
|
|
1456
1509
|
const isExploratory = Boolean(governedAnswer.exploratoryCandidate);
|
|
1457
1510
|
const isExecutionFailure = agentAnswerHasExecutionFailure(governedAnswer);
|
|
1458
1511
|
const isGroundingGap = governedAnswer.kind === 'no_answer'
|
|
1459
|
-
&& governedAnswer.refusalCode === 'grounding_gap'
|
|
1512
|
+
&& (governedAnswer.refusalCode === 'grounding_gap' || governedAnswer.refusalCode === 'modeling_gap')
|
|
1460
1513
|
&& !isExploratory;
|
|
1461
1514
|
const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
|
|
1462
1515
|
// AGT-004: a rejected attribution/export/proof policy is a deliberate
|
|
@@ -1786,6 +1839,7 @@ export async function startLocalServer(opts) {
|
|
|
1786
1839
|
}
|
|
1787
1840
|
}
|
|
1788
1841
|
catch (error) {
|
|
1842
|
+
rethrowIfCancelled(error, request.signal);
|
|
1789
1843
|
researchWorkspaceError = formatNotebookResearchStorageError(error);
|
|
1790
1844
|
}
|
|
1791
1845
|
}
|
|
@@ -2355,7 +2409,13 @@ export async function startLocalServer(opts) {
|
|
|
2355
2409
|
getEvidence: buildAgentRunEvidence,
|
|
2356
2410
|
getCatalogContext: buildRankedAgentRunCatalogContext,
|
|
2357
2411
|
});
|
|
2358
|
-
|
|
2412
|
+
// P0: one row per run with retention + old-run compaction. The legacy JSON
|
|
2413
|
+
// store rewrote the entire file (123 MB observed) twice per answered question;
|
|
2414
|
+
// existing history is imported once and the JSON renamed to *.migrated.
|
|
2415
|
+
const agentRunStore = new SqliteAgentRunStore({
|
|
2416
|
+
path: defaultAgentRunSqlitePath(projectRoot),
|
|
2417
|
+
legacyJsonPath: defaultAgentRunStorePath(projectRoot),
|
|
2418
|
+
});
|
|
2359
2419
|
// A run may outlive its streaming browser connection, so cancellation is
|
|
2360
2420
|
// server-owned and keyed by run id rather than relying on fetch abort alone.
|
|
2361
2421
|
const activeAgentRunControllers = new Map();
|
|
@@ -2425,13 +2485,28 @@ export async function startLocalServer(opts) {
|
|
|
2425
2485
|
const activeConnection = requireActiveConnection();
|
|
2426
2486
|
const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
|
|
2427
2487
|
const plan = buildExecutionPlan(resolved.cell, { semanticLayer, driver: activeConnection.driver, tableMapping });
|
|
2428
|
-
|
|
2488
|
+
const semanticCompose = resolved.cell.type === 'dql'
|
|
2489
|
+
&& semanticLayer
|
|
2490
|
+
&& /\btype\s*=\s*"semantic"/i.test(resolved.cell.source)
|
|
2491
|
+
? await composeSemanticBlockSqlForRuntime(resolved.cell.source, semanticLayer, {
|
|
2492
|
+
driver: activeConnection.driver,
|
|
2493
|
+
tableMapping,
|
|
2494
|
+
detectedProvider: semanticDetectedProvider,
|
|
2495
|
+
projectRoot,
|
|
2496
|
+
projectConfig,
|
|
2497
|
+
})
|
|
2498
|
+
: null;
|
|
2499
|
+
if (semanticCompose && !semanticCompose.sql) {
|
|
2500
|
+
throw new Error(semanticCompose.diagnostics.map((diagnostic) => diagnostic.message).join(' '));
|
|
2501
|
+
}
|
|
2502
|
+
const executableSql = semanticCompose?.sql ?? plan?.sql;
|
|
2503
|
+
if (!executableSql) {
|
|
2429
2504
|
snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
|
|
2430
2505
|
continue;
|
|
2431
2506
|
}
|
|
2432
|
-
const prepared = prepareLocalExecution(
|
|
2507
|
+
const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
|
|
2433
2508
|
assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
|
|
2434
|
-
const rawResult = await executor.executeQuery(prepared.sql, plan
|
|
2509
|
+
const rawResult = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), prepared.connection);
|
|
2435
2510
|
const result = normalizeQueryResult(rawResult);
|
|
2436
2511
|
snapshotCells.push({
|
|
2437
2512
|
cellId,
|
|
@@ -2478,6 +2553,7 @@ export async function startLocalServer(opts) {
|
|
|
2478
2553
|
const invocation = prepareBlockInvocation({
|
|
2479
2554
|
source,
|
|
2480
2555
|
parameters: invocationInput?.parameters,
|
|
2556
|
+
parameterSources: invocationInput?.parameterSources,
|
|
2481
2557
|
question: invocationInput?.question,
|
|
2482
2558
|
surface: 'ask_ai',
|
|
2483
2559
|
});
|
|
@@ -2490,7 +2566,7 @@ export async function startLocalServer(opts) {
|
|
|
2490
2566
|
const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
|
|
2491
2567
|
const plan = buildExecutionPlan({ id: `agent-${metadata.name ?? 'dql-artifact'}`, type: 'dql', source, title: metadata.name ?? 'DQL artifact' }, { semanticLayer, driver: activeConnection.driver, tableMapping, parameters: invocation.values });
|
|
2492
2568
|
const semanticCompose = semanticLayer
|
|
2493
|
-
?
|
|
2569
|
+
? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
|
|
2494
2570
|
driver: activeConnection.driver,
|
|
2495
2571
|
tableMapping,
|
|
2496
2572
|
projectRoot,
|
|
@@ -2574,10 +2650,11 @@ export async function startLocalServer(opts) {
|
|
|
2574
2650
|
* candidate remains review-required and is not executed automatically.
|
|
2575
2651
|
*/
|
|
2576
2652
|
const executeExploratoryCandidate = async (candidate, schemaContext = [], question = '', requestedTopN) => {
|
|
2577
|
-
const
|
|
2653
|
+
const activeConnection = requireActiveConnection();
|
|
2654
|
+
const preflight = repairExploratorySqlBeforeExecution(candidate.sql, schemaContext, question, activeConnection.driver);
|
|
2578
2655
|
const boundedSql = applyRequestedTopNToExploratorySql(preflight.sql, requestedTopN);
|
|
2579
2656
|
const repairs = boundedSql === preflight.sql
|
|
2580
|
-
? preflight.repairs
|
|
2657
|
+
? [...preflight.repairs]
|
|
2581
2658
|
: [...preflight.repairs, `Applied the requested overall top-${requestedTopN} bound before exploratory execution.`];
|
|
2582
2659
|
if (preflight.blockedReason) {
|
|
2583
2660
|
return {
|
|
@@ -2587,23 +2664,65 @@ export async function startLocalServer(opts) {
|
|
|
2587
2664
|
error: preflight.blockedReason,
|
|
2588
2665
|
};
|
|
2589
2666
|
}
|
|
2590
|
-
const analysis = analyzeSqlReferences(boundedSql);
|
|
2667
|
+
const analysis = analyzeSqlReferences(boundedSql, activeConnection.driver);
|
|
2591
2668
|
if (!analysis.parsed) {
|
|
2592
2669
|
return { proofs: [], sql: boundedSql, repairs, error: 'DQL could not parse the exploratory SQL to validate its join predicates.' };
|
|
2593
2670
|
}
|
|
2671
|
+
const probeableJoins = probeableExploratoryJoins(analysis.joins, analysis.ctes);
|
|
2594
2672
|
// A normal customer → order → order-item → product question needs three
|
|
2595
2673
|
// joins. Keep the lane bounded, but do not reject this common dbt-star path
|
|
2596
|
-
// solely because the old two-join demo limit was too small.
|
|
2597
|
-
|
|
2674
|
+
// solely because the old two-join demo limit was too small. CTE-internal
|
|
2675
|
+
// joins are counted physically (they appear in analysis.joins with their
|
|
2676
|
+
// physical endpoints); joins ON a CTE are restructuring, not new paths.
|
|
2677
|
+
if (probeableJoins.length > 4) {
|
|
2598
2678
|
return { proofs: [], sql: boundedSql, repairs, error: 'This exploratory query has more than four joins and requires an analyst to review the relationship path.' };
|
|
2599
2679
|
}
|
|
2600
|
-
if (
|
|
2680
|
+
if (probeableJoins.some((join) => !join.leftRelation || !join.rightRelation)) {
|
|
2601
2681
|
return { proofs: [], sql: boundedSql, repairs, error: 'This exploratory query has a join endpoint DQL could not resolve for bounded validation.' };
|
|
2602
2682
|
}
|
|
2603
|
-
|
|
2683
|
+
// Structural binding: when the candidate carries a declared draft join path,
|
|
2684
|
+
// every SQL join must ride one of its edges on the declared key pair. A join
|
|
2685
|
+
// outside the declared path (or on different keys) is not executed — it is a
|
|
2686
|
+
// review-required modeling question, not a runtime guess.
|
|
2687
|
+
const declaredEdges = candidate.exploratoryPath?.edges ?? [];
|
|
2688
|
+
const joinEdges = new Map();
|
|
2689
|
+
if (declaredEdges.length > 0) {
|
|
2690
|
+
for (const [index, join] of probeableJoins.entries()) {
|
|
2691
|
+
const edge = declaredEdges.find((value) => declaredExploratoryRelationMatches(join.leftRelation, value.fromRelation) && declaredExploratoryRelationMatches(join.rightRelation, value.toRelation)
|
|
2692
|
+
|| declaredExploratoryRelationMatches(join.leftRelation, value.toRelation) && declaredExploratoryRelationMatches(join.rightRelation, value.fromRelation));
|
|
2693
|
+
if (!edge) {
|
|
2694
|
+
return {
|
|
2695
|
+
proofs: [],
|
|
2696
|
+
sql: boundedSql,
|
|
2697
|
+
repairs,
|
|
2698
|
+
error: `The join between ${join.leftRelation} and ${join.rightRelation} is not part of the declared relationship path, so DQL did not execute it. Declare the relationship in the DQL model to enable it.`,
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
const keyMatches = edge.keys.some((key) => exploratoryColumnsMatch(join.leftColumn, key.from) && exploratoryColumnsMatch(join.rightColumn, key.to)
|
|
2702
|
+
|| exploratoryColumnsMatch(join.leftColumn, key.to) && exploratoryColumnsMatch(join.rightColumn, key.from));
|
|
2703
|
+
if (!keyMatches) {
|
|
2704
|
+
return {
|
|
2705
|
+
proofs: [],
|
|
2706
|
+
sql: boundedSql,
|
|
2707
|
+
repairs,
|
|
2708
|
+
error: `The join ${join.leftColumn} = ${join.rightColumn} uses different keys than relationship "${edge.relationshipId}" declares (${edge.keys.map((key) => `${key.from}=${key.to}`).join(', ')}). Review the declared keys before executing.`,
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
joinEdges.set(index, edge);
|
|
2712
|
+
}
|
|
2713
|
+
const exercised = new Set([...joinEdges.values()].map((edge) => edge.relationshipId));
|
|
2714
|
+
for (const edge of declaredEdges) {
|
|
2715
|
+
if (!exercised.has(edge.relationshipId)) {
|
|
2716
|
+
repairs.push(`Declared relationship "${edge.relationshipId}" was not exercised by this query.`);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (probeableJoins.length < analysis.joins.length) {
|
|
2721
|
+
repairs.push(`Skipped join probes for ${analysis.joins.length - probeableJoins.length} join(s) on derived (CTE) relations; the final query execution validates them.`);
|
|
2722
|
+
}
|
|
2604
2723
|
const proofs = [];
|
|
2605
2724
|
try {
|
|
2606
|
-
for (const join of
|
|
2725
|
+
for (const [index, join] of probeableJoins.entries()) {
|
|
2607
2726
|
const proofSql = buildExploratoryJoinProbeSql({
|
|
2608
2727
|
leftRelation: join.leftRelation,
|
|
2609
2728
|
leftColumn: join.leftColumn,
|
|
@@ -2613,6 +2732,14 @@ export async function startLocalServer(opts) {
|
|
|
2613
2732
|
const prepared = prepareLocalExecution(proofSql, activeConnection, projectRoot, projectConfig);
|
|
2614
2733
|
const raw = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
|
|
2615
2734
|
proofs.push({ summary: summarizeExploratoryJoinProbe(raw.rows, join.leftRelation, join.leftColumn, join.rightRelation, join.rightColumn) });
|
|
2735
|
+
// Gating (not observational): a structural contradiction between the
|
|
2736
|
+
// UNFILTERED key overlap and the declared relationship stops automatic
|
|
2737
|
+
// execution. A legitimately empty *filtered* business result is still an
|
|
2738
|
+
// answer — these gates only inspect the raw key samples.
|
|
2739
|
+
const contradiction = exploratoryProbeContradiction(raw.rows, join, joinEdges.get(index));
|
|
2740
|
+
if (contradiction) {
|
|
2741
|
+
return { proofs, sql: boundedSql, repairs, error: contradiction };
|
|
2742
|
+
}
|
|
2616
2743
|
}
|
|
2617
2744
|
const result = await executeGeneratedSqlForAgent(boundedSql);
|
|
2618
2745
|
return { result, proofs, sql: boundedSql, repairs };
|
|
@@ -3461,7 +3588,7 @@ export async function startLocalServer(opts) {
|
|
|
3461
3588
|
throw new Error(`Provide required parameter${invocation.unresolvedParameters.length === 1 ? '' : 's'}: ${invocation.unresolvedParameters.join(', ')}.`);
|
|
3462
3589
|
}
|
|
3463
3590
|
const semanticCompose = semanticLayer
|
|
3464
|
-
?
|
|
3591
|
+
? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
|
|
3465
3592
|
driver: activeConnection.driver,
|
|
3466
3593
|
tableMapping,
|
|
3467
3594
|
projectRoot,
|
|
@@ -3515,7 +3642,7 @@ export async function startLocalServer(opts) {
|
|
|
3515
3642
|
// pre-compiled query, that's the query (not a recompiled metric), so the test's
|
|
3516
3643
|
// output columns match the block's declared outputs.
|
|
3517
3644
|
const semanticCompose = semanticLayer
|
|
3518
|
-
?
|
|
3645
|
+
? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
|
|
3519
3646
|
driver: activeConnection.driver,
|
|
3520
3647
|
tableMapping,
|
|
3521
3648
|
projectRoot,
|
|
@@ -3733,8 +3860,19 @@ export async function startLocalServer(opts) {
|
|
|
3733
3860
|
}
|
|
3734
3861
|
}
|
|
3735
3862
|
if (req.method === 'GET' && path === '/api/health') {
|
|
3863
|
+
// REL-002: expose runtime/build identity so a server started before a
|
|
3864
|
+
// rebuild or running a different version than the project pin is visibly
|
|
3865
|
+
// stale. Uses only the cached latest-version lookup — never blocks.
|
|
3866
|
+
const versionStatus = resolveDqlRuntimeVersionStatus({ projectRoot, runningVersion: runtimeVersion });
|
|
3867
|
+
void fetchLatestPublishedDqlVersion();
|
|
3868
|
+
const healthValueGrounding = resolveAgentRuntimeValueGrounding(projectConfig);
|
|
3869
|
+
const retrievalHealth = resolveRetrievalHealthStatus({
|
|
3870
|
+
projectRoot,
|
|
3871
|
+
valueGroundingMode: healthValueGrounding.mode,
|
|
3872
|
+
searchSafeColumnCount: healthValueGrounding.searchSafeColumns.size,
|
|
3873
|
+
});
|
|
3736
3874
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3737
|
-
res.end(serializeJSON({ status: 'ok', version: runtimeVersion }));
|
|
3875
|
+
res.end(serializeJSON({ status: 'ok', version: runtimeVersion, versionStatus, retrievalHealth }));
|
|
3738
3876
|
return;
|
|
3739
3877
|
}
|
|
3740
3878
|
if (req.method === 'GET' && path === '/api/onboarding/launch') {
|
|
@@ -4543,6 +4681,11 @@ export async function startLocalServer(opts) {
|
|
|
4543
4681
|
const conversationStore = parsed.request.threadId ? getConversationStore() : null;
|
|
4544
4682
|
if (conversationStore && parsed.request.threadId && conversationStore.getThread(parsed.request.threadId)) {
|
|
4545
4683
|
parsed.request.conversationContext = await conversationContextFromThread(conversationStore, parsed.request.threadId, parsed.request.conversationContext, parsed.request.question);
|
|
4684
|
+
// The persisted thread is authoritative for prior turns. Raw client
|
|
4685
|
+
// history would duplicate the same conversation into the prompt a
|
|
4686
|
+
// second time — dropping it keeps follow-up prompts bounded.
|
|
4687
|
+
if (parsed.request.history?.length)
|
|
4688
|
+
parsed.request.history = [];
|
|
4546
4689
|
}
|
|
4547
4690
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
4548
4691
|
const runId = parsed.request.runId;
|
|
@@ -5917,7 +6060,7 @@ export async function startLocalServer(opts) {
|
|
|
5917
6060
|
operator: filter.operator,
|
|
5918
6061
|
values: Array.isArray(filter.value) ? filter.value.map(String) : [String(filter.value)],
|
|
5919
6062
|
}));
|
|
5920
|
-
const composed = composeRuntimeSemanticQuery({
|
|
6063
|
+
const composed = await composeRuntimeSemanticQuery({
|
|
5921
6064
|
metrics: item.semantic.metrics,
|
|
5922
6065
|
dimensions: item.semantic.dimensions ?? [],
|
|
5923
6066
|
filters: [...staticFilters, ...activeFilters],
|
|
@@ -6008,7 +6151,7 @@ export async function startLocalServer(opts) {
|
|
|
6008
6151
|
continue;
|
|
6009
6152
|
}
|
|
6010
6153
|
const semanticCompose = semanticLayer
|
|
6011
|
-
?
|
|
6154
|
+
? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
|
|
6012
6155
|
driver: targetConnection.driver,
|
|
6013
6156
|
tableMapping,
|
|
6014
6157
|
projectRoot,
|
|
@@ -7387,7 +7530,7 @@ export async function startLocalServer(opts) {
|
|
|
7387
7530
|
const activeConnection = requireActiveConnection();
|
|
7388
7531
|
const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
|
|
7389
7532
|
const semanticCompose = semanticLayer
|
|
7390
|
-
?
|
|
7533
|
+
? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
|
|
7391
7534
|
driver: activeConnection.driver,
|
|
7392
7535
|
tableMapping,
|
|
7393
7536
|
projectRoot,
|
|
@@ -7893,8 +8036,23 @@ export async function startLocalServer(opts) {
|
|
|
7893
8036
|
draftSave: readiness.candidate.draftSave ?? { status: 'pending' },
|
|
7894
8037
|
};
|
|
7895
8038
|
writeBlockStudioImportCandidate(projectRoot, importId, next);
|
|
7896
|
-
|
|
7897
|
-
|
|
8039
|
+
let compiledManifest;
|
|
8040
|
+
let lineageRefresh;
|
|
8041
|
+
try {
|
|
8042
|
+
compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
|
|
8043
|
+
lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
|
|
8044
|
+
}
|
|
8045
|
+
catch (error) {
|
|
8046
|
+
lineageRefresh = {
|
|
8047
|
+
status: 'failed',
|
|
8048
|
+
message: error instanceof Error ? error.message : String(error),
|
|
8049
|
+
};
|
|
8050
|
+
}
|
|
8051
|
+
await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
|
|
8052
|
+
const payload = {
|
|
8053
|
+
...openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
|
|
8054
|
+
lineageRefresh,
|
|
8055
|
+
};
|
|
7898
8056
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7899
8057
|
res.end(serializeJSON({ candidate: next, block: payload, certification }));
|
|
7900
8058
|
return;
|
|
@@ -8007,10 +8165,23 @@ export async function startLocalServer(opts) {
|
|
|
8007
8165
|
}
|
|
8008
8166
|
const nextSession = { ...session, candidates: nextCandidates, updatedAt: new Date().toISOString() };
|
|
8009
8167
|
writeBlockStudioImportSession(projectRoot, nextSession);
|
|
8010
|
-
|
|
8011
|
-
|
|
8168
|
+
let lineageRefresh;
|
|
8169
|
+
if (saved.length > 0) {
|
|
8170
|
+
let compiledManifest;
|
|
8171
|
+
try {
|
|
8172
|
+
compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
|
|
8173
|
+
lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
|
|
8174
|
+
}
|
|
8175
|
+
catch (error) {
|
|
8176
|
+
lineageRefresh = {
|
|
8177
|
+
status: 'failed',
|
|
8178
|
+
message: error instanceof Error ? error.message : String(error),
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
|
|
8182
|
+
}
|
|
8012
8183
|
res.writeHead(errors.length > 0 ? 207 : 200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8013
|
-
res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors }));
|
|
8184
|
+
res.end(serializeJSON({ ok: errors.length === 0, session: nextSession, saved, errors, lineageRefresh }));
|
|
8014
8185
|
}
|
|
8015
8186
|
catch (error) {
|
|
8016
8187
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -8154,9 +8325,14 @@ export async function startLocalServer(opts) {
|
|
|
8154
8325
|
const connections = getProjectConnectionsForApi(cfg);
|
|
8155
8326
|
const defaultKey = resolveDefaultConnectionKey(cfg, connections) ?? Object.keys(connections)[0] ?? 'default';
|
|
8156
8327
|
const userPrefs = readUserPrefs(userPrefsPath);
|
|
8328
|
+
// UI-009 / PERF-001: Block Studio already loads the canonical semantic
|
|
8329
|
+
// layer. Let it omit this second, potentially multi-megabyte rendering
|
|
8330
|
+
// of the same 7,500+ object catalog while keeping the route compatible
|
|
8331
|
+
// for older clients.
|
|
8332
|
+
const includeSemantic = url.searchParams.get('includeSemantic') !== 'false';
|
|
8157
8333
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8158
8334
|
res.end(serializeJSON({
|
|
8159
|
-
semanticTree: semanticLayer ? buildSemanticTree(semanticLayer, semanticImportManifest) : null,
|
|
8335
|
+
semanticTree: includeSemantic && semanticLayer ? buildSemanticTree(semanticLayer, semanticImportManifest) : null,
|
|
8160
8336
|
databaseTree: await buildDatabaseSchemaTree(projectRoot, executor, connection),
|
|
8161
8337
|
connection: {
|
|
8162
8338
|
default: defaultKey,
|
|
@@ -8317,8 +8493,26 @@ export async function startLocalServer(opts) {
|
|
|
8317
8493
|
stableSuffix: metadata.candidateId,
|
|
8318
8494
|
})
|
|
8319
8495
|
: saveBlockStudioArtifacts(projectRoot, saveOptions);
|
|
8320
|
-
|
|
8321
|
-
|
|
8496
|
+
let compiledManifest;
|
|
8497
|
+
let lineageRefresh;
|
|
8498
|
+
try {
|
|
8499
|
+
compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
|
|
8500
|
+
lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
|
|
8501
|
+
}
|
|
8502
|
+
catch (error) {
|
|
8503
|
+
// The block is already safely stored. Preserve it and report the
|
|
8504
|
+
// rebuild failure separately instead of turning a successful save
|
|
8505
|
+
// into a misleading 500 response.
|
|
8506
|
+
lineageRefresh = {
|
|
8507
|
+
status: 'failed',
|
|
8508
|
+
message: error instanceof Error ? error.message : String(error),
|
|
8509
|
+
};
|
|
8510
|
+
}
|
|
8511
|
+
await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
|
|
8512
|
+
const payload = {
|
|
8513
|
+
...openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
|
|
8514
|
+
lineageRefresh,
|
|
8515
|
+
};
|
|
8322
8516
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8323
8517
|
res.end(serializeJSON(payload));
|
|
8324
8518
|
}
|
|
@@ -8717,6 +8911,47 @@ export async function startLocalServer(opts) {
|
|
|
8717
8911
|
}
|
|
8718
8912
|
return;
|
|
8719
8913
|
}
|
|
8914
|
+
// ── Semantic runtime adapters (API-004 / UI-009 / E2E-008) ───────────────
|
|
8915
|
+
if (req.method === 'GET' && path === '/api/semantic-runtime') {
|
|
8916
|
+
try {
|
|
8917
|
+
const runtime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
|
|
8918
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8919
|
+
res.end(serializeJSON({ ...getSemanticRuntimeSettings(projectRoot), runtime }));
|
|
8920
|
+
}
|
|
8921
|
+
catch (error) {
|
|
8922
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8923
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
8924
|
+
}
|
|
8925
|
+
return;
|
|
8926
|
+
}
|
|
8927
|
+
if (req.method === 'POST' && path === '/api/semantic-runtime/dbt-cloud/test') {
|
|
8928
|
+
try {
|
|
8929
|
+
const body = await readJSON(req);
|
|
8930
|
+
const result = await testSemanticRuntimeDraft(projectRoot, body);
|
|
8931
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8932
|
+
res.end(serializeJSON(result));
|
|
8933
|
+
}
|
|
8934
|
+
catch (error) {
|
|
8935
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8936
|
+
res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
8937
|
+
}
|
|
8938
|
+
return;
|
|
8939
|
+
}
|
|
8940
|
+
if (req.method === 'POST' && path === '/api/semantic-runtime/dbt-cloud/apply') {
|
|
8941
|
+
try {
|
|
8942
|
+
const body = await readJSON(req);
|
|
8943
|
+
const result = await testSemanticRuntimeDraft(projectRoot, body);
|
|
8944
|
+
const settings = saveTestedSemanticRuntimeSettings(projectRoot, body, result);
|
|
8945
|
+
const runtime = await getSemanticRuntimeStatus(projectRoot);
|
|
8946
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8947
|
+
res.end(serializeJSON({ ok: true, ...settings, runtime }));
|
|
8948
|
+
}
|
|
8949
|
+
catch (error) {
|
|
8950
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8951
|
+
res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
8952
|
+
}
|
|
8953
|
+
return;
|
|
8954
|
+
}
|
|
8720
8955
|
// ── Semantic layer discovery API ─────────────────────────────────────────
|
|
8721
8956
|
if (req.method === 'GET' && path === '/api/semantic-layer') {
|
|
8722
8957
|
const userPrefs = readUserPrefs(userPrefsPath);
|
|
@@ -8746,8 +8981,7 @@ export async function startLocalServer(opts) {
|
|
|
8746
8981
|
const dbtManifestReady = provider === 'dbt'
|
|
8747
8982
|
? hasDbtSemanticManifest(projectRoot, semanticConfig?.projectPath)
|
|
8748
8983
|
: false;
|
|
8749
|
-
const
|
|
8750
|
-
const dbtExecutionReady = dbtManifestReady && metricFlowReady;
|
|
8984
|
+
const semanticRuntime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
|
|
8751
8985
|
const metrics = semanticLayer.listMetrics().map((m) => ({
|
|
8752
8986
|
name: m.name,
|
|
8753
8987
|
label: m.label,
|
|
@@ -8762,7 +8996,7 @@ export async function startLocalServer(opts) {
|
|
|
8762
8996
|
typeParams: m.typeParams ?? null,
|
|
8763
8997
|
filter: m.filter ?? null,
|
|
8764
8998
|
source: m.source ?? null,
|
|
8765
|
-
execution:
|
|
8999
|
+
execution: runtimeMetricExecutionCapability(m.name, semanticLayer, provider, semanticRuntime),
|
|
8766
9000
|
}));
|
|
8767
9001
|
const measures = semanticLayer.listMeasures().map((m) => ({
|
|
8768
9002
|
name: m.name,
|
|
@@ -8860,24 +9094,21 @@ export async function startLocalServer(opts) {
|
|
|
8860
9094
|
owner: q.owner ?? null,
|
|
8861
9095
|
source: q.source ?? null,
|
|
8862
9096
|
}));
|
|
8863
|
-
const dbtExecutionSetup =
|
|
8864
|
-
?
|
|
8865
|
-
:
|
|
8866
|
-
? 'Run `dbt parse` or `dbt build` so target/semantic_manifest.json exists, and install MetricFlow so `mf` is on PATH.'
|
|
8867
|
-
: !dbtManifestReady
|
|
8868
|
-
? 'Run `dbt parse` or `dbt build` so target/semantic_manifest.json exists.'
|
|
8869
|
-
: 'Install MetricFlow so `mf` is on PATH, or set DQL_METRICFLOW_BIN to the MetricFlow executable.';
|
|
9097
|
+
const dbtExecutionSetup = !dbtManifestReady
|
|
9098
|
+
? 'Run `dbt parse` or `dbt build` so target/semantic_manifest.json exists.'
|
|
9099
|
+
: semanticRuntime.setup;
|
|
8870
9100
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8871
9101
|
res.end(serializeJSON({
|
|
8872
9102
|
available: true,
|
|
8873
9103
|
provider,
|
|
8874
9104
|
execution: provider === 'dbt'
|
|
8875
9105
|
? {
|
|
8876
|
-
engine:
|
|
8877
|
-
ready:
|
|
9106
|
+
engine: semanticRuntime.active,
|
|
9107
|
+
ready: semanticRuntime.active !== 'native' || metrics.every((metric) => metric.execution.status === 'ready'),
|
|
8878
9108
|
setup: dbtExecutionSetup,
|
|
9109
|
+
adapters: semanticRuntime.adapters,
|
|
8879
9110
|
}
|
|
8880
|
-
: { engine: 'native', ready: true, setup: null },
|
|
9111
|
+
: { engine: 'native', ready: true, setup: null, adapters: semanticRuntime.adapters },
|
|
8881
9112
|
errors: semanticLayerErrors,
|
|
8882
9113
|
metrics,
|
|
8883
9114
|
measures,
|
|
@@ -9214,7 +9445,7 @@ export async function startLocalServer(opts) {
|
|
|
9214
9445
|
.split(',')
|
|
9215
9446
|
.map((value) => value.trim())
|
|
9216
9447
|
.filter(Boolean);
|
|
9217
|
-
const dimensions = semanticLayer
|
|
9448
|
+
const dimensions = (await listRuntimeCompatibleDimensions(projectRoot, semanticLayer, metrics)).map((d) => ({
|
|
9218
9449
|
name: d.name,
|
|
9219
9450
|
label: d.label,
|
|
9220
9451
|
description: d.description,
|
|
@@ -9727,37 +9958,8 @@ export async function startLocalServer(opts) {
|
|
|
9727
9958
|
// Resolve which connection to use — request can override default
|
|
9728
9959
|
const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
|
|
9729
9960
|
const driver = targetConnection.driver;
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
try {
|
|
9733
|
-
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name FROM information_schema.tables WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')`, [], {}, targetConnection);
|
|
9734
|
-
const dbTableNames = new Set();
|
|
9735
|
-
const schemaQualified = new Map();
|
|
9736
|
-
for (const row of tablesResult.rows) {
|
|
9737
|
-
const schema = String(row['table_schema'] ?? '');
|
|
9738
|
-
const name = String(row['table_name'] ?? '');
|
|
9739
|
-
dbTableNames.add(name);
|
|
9740
|
-
schemaQualified.set(name, schema ? `${schema}.${name}` : name);
|
|
9741
|
-
}
|
|
9742
|
-
// For each table in the semantic layer, map to qualified name if it exists
|
|
9743
|
-
const allSemanticTables = new Set();
|
|
9744
|
-
for (const m of semanticLayer.listMetrics())
|
|
9745
|
-
allSemanticTables.add(m.table);
|
|
9746
|
-
for (const d of semanticLayer.listDimensions())
|
|
9747
|
-
allSemanticTables.add(d.table);
|
|
9748
|
-
tableMapping = {};
|
|
9749
|
-
for (const semTable of allSemanticTables) {
|
|
9750
|
-
if (dbTableNames.has(semTable) && schemaQualified.has(semTable)) {
|
|
9751
|
-
tableMapping[semTable] = schemaQualified.get(semTable);
|
|
9752
|
-
}
|
|
9753
|
-
}
|
|
9754
|
-
if (Object.keys(tableMapping).length === 0)
|
|
9755
|
-
tableMapping = undefined;
|
|
9756
|
-
}
|
|
9757
|
-
catch {
|
|
9758
|
-
// Non-fatal: proceed without table mapping
|
|
9759
|
-
}
|
|
9760
|
-
const composed = composeRuntimeSemanticQuery({
|
|
9961
|
+
const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
|
|
9962
|
+
const composed = await composeRuntimeSemanticQuery({
|
|
9761
9963
|
metrics,
|
|
9762
9964
|
dimensions,
|
|
9763
9965
|
filters,
|
|
@@ -9775,10 +9977,10 @@ export async function startLocalServer(opts) {
|
|
|
9775
9977
|
});
|
|
9776
9978
|
if (!composed) {
|
|
9777
9979
|
const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
|
|
9778
|
-
const
|
|
9980
|
+
const semanticRuntime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
|
|
9779
9981
|
const blocked = metrics.map((metricName) => ({
|
|
9780
9982
|
metric: metricName,
|
|
9781
|
-
...
|
|
9983
|
+
...runtimeMetricExecutionCapability(metricName, semanticLayer, provider, semanticRuntime),
|
|
9782
9984
|
})).filter((capability) => capability.status !== 'ready');
|
|
9783
9985
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9784
9986
|
res.end(serializeJSON({
|
|
@@ -9808,13 +10010,14 @@ export async function startLocalServer(opts) {
|
|
|
9808
10010
|
res.end(serializeJSON({ error: error.message, code: 'unauthorized' }));
|
|
9809
10011
|
return;
|
|
9810
10012
|
}
|
|
9811
|
-
const
|
|
10013
|
+
const runtimeError = isSemanticRuntimeError(error);
|
|
10014
|
+
const status = runtimeError ? 400 : 500;
|
|
9812
10015
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9813
10016
|
res.end(serializeJSON({
|
|
9814
10017
|
error: error instanceof Error ? error.message : String(error),
|
|
9815
|
-
code:
|
|
9816
|
-
hint:
|
|
9817
|
-
? '
|
|
10018
|
+
code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
|
|
10019
|
+
hint: runtimeError
|
|
10020
|
+
? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
|
|
9818
10021
|
: undefined,
|
|
9819
10022
|
}));
|
|
9820
10023
|
}
|
|
@@ -9831,31 +10034,8 @@ export async function startLocalServer(opts) {
|
|
|
9831
10034
|
const { metrics = [], dimensions = [], filters = [], limit, timeDimension, orderBy, savedQuery, engine } = body;
|
|
9832
10035
|
const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
|
|
9833
10036
|
const driver = targetConnection.driver;
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name FROM information_schema.tables WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')`, [], {}, targetConnection);
|
|
9837
|
-
const schemaQualified = new Map();
|
|
9838
|
-
for (const row of tablesResult.rows) {
|
|
9839
|
-
const schema = String(row['table_schema'] ?? '');
|
|
9840
|
-
const name = String(row['table_name'] ?? '');
|
|
9841
|
-
schemaQualified.set(name, schema ? `${schema}.${name}` : name);
|
|
9842
|
-
}
|
|
9843
|
-
tableMapping = {};
|
|
9844
|
-
for (const metric of semanticLayer.listMetrics()) {
|
|
9845
|
-
if (schemaQualified.has(metric.table))
|
|
9846
|
-
tableMapping[metric.table] = schemaQualified.get(metric.table);
|
|
9847
|
-
}
|
|
9848
|
-
for (const dimension of semanticLayer.listDimensions()) {
|
|
9849
|
-
if (schemaQualified.has(dimension.table))
|
|
9850
|
-
tableMapping[dimension.table] = schemaQualified.get(dimension.table);
|
|
9851
|
-
}
|
|
9852
|
-
if (Object.keys(tableMapping).length === 0)
|
|
9853
|
-
tableMapping = undefined;
|
|
9854
|
-
}
|
|
9855
|
-
catch {
|
|
9856
|
-
tableMapping = undefined;
|
|
9857
|
-
}
|
|
9858
|
-
const composed = composeRuntimeSemanticQuery({
|
|
10037
|
+
const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
|
|
10038
|
+
const composed = await composeRuntimeSemanticQuery({
|
|
9859
10039
|
metrics,
|
|
9860
10040
|
dimensions,
|
|
9861
10041
|
filters,
|
|
@@ -9873,10 +10053,10 @@ export async function startLocalServer(opts) {
|
|
|
9873
10053
|
});
|
|
9874
10054
|
if (!composed) {
|
|
9875
10055
|
const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
|
|
9876
|
-
const
|
|
10056
|
+
const semanticRuntime = await getSemanticRuntimeStatus(projectRoot, { probeConfiguredCloud: true });
|
|
9877
10057
|
const blocked = metrics.map((metricName) => ({
|
|
9878
10058
|
metric: metricName,
|
|
9879
|
-
...
|
|
10059
|
+
...runtimeMetricExecutionCapability(metricName, semanticLayer, provider, semanticRuntime),
|
|
9880
10060
|
})).filter((capability) => capability.status !== 'ready');
|
|
9881
10061
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9882
10062
|
res.end(serializeJSON({
|
|
@@ -9900,13 +10080,14 @@ export async function startLocalServer(opts) {
|
|
|
9900
10080
|
}));
|
|
9901
10081
|
}
|
|
9902
10082
|
catch (error) {
|
|
9903
|
-
const
|
|
10083
|
+
const runtimeError = isSemanticRuntimeError(error);
|
|
10084
|
+
const status = runtimeError ? 400 : 500;
|
|
9904
10085
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9905
10086
|
res.end(serializeJSON({
|
|
9906
10087
|
error: error instanceof Error ? error.message : String(error),
|
|
9907
|
-
code:
|
|
9908
|
-
hint:
|
|
9909
|
-
? '
|
|
10088
|
+
code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
|
|
10089
|
+
hint: runtimeError
|
|
10090
|
+
? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
|
|
9910
10091
|
: undefined,
|
|
9911
10092
|
}));
|
|
9912
10093
|
}
|
|
@@ -9931,7 +10112,8 @@ export async function startLocalServer(opts) {
|
|
|
9931
10112
|
return;
|
|
9932
10113
|
}
|
|
9933
10114
|
const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
|
|
9934
|
-
const
|
|
10115
|
+
const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
|
|
10116
|
+
const composed = await composeRuntimeSemanticQuery({
|
|
9935
10117
|
metrics,
|
|
9936
10118
|
dimensions,
|
|
9937
10119
|
filters,
|
|
@@ -9942,6 +10124,7 @@ export async function startLocalServer(opts) {
|
|
|
9942
10124
|
projectConfig,
|
|
9943
10125
|
detectedProvider: semanticDetectedProvider,
|
|
9944
10126
|
driver: targetConnection.driver,
|
|
10127
|
+
tableMapping,
|
|
9945
10128
|
});
|
|
9946
10129
|
if (!composed) {
|
|
9947
10130
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -9972,13 +10155,14 @@ export async function startLocalServer(opts) {
|
|
|
9972
10155
|
res.end(serializeJSON({ error: 'Block already exists' }));
|
|
9973
10156
|
return;
|
|
9974
10157
|
}
|
|
9975
|
-
const
|
|
10158
|
+
const runtimeError = isSemanticRuntimeError(error);
|
|
10159
|
+
const status = runtimeError ? 400 : 500;
|
|
9976
10160
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9977
10161
|
res.end(serializeJSON({
|
|
9978
10162
|
error: error instanceof Error ? error.message : String(error),
|
|
9979
|
-
code:
|
|
9980
|
-
hint:
|
|
9981
|
-
? '
|
|
10163
|
+
code: runtimeError ? 'SEMANTIC_RUNTIME_REQUIRED' : undefined,
|
|
10164
|
+
hint: runtimeError
|
|
10165
|
+
? 'Configure dbt Cloud Semantic Layer in Project & dbt settings, or install a compatible local MetricFlow runtime.'
|
|
9982
10166
|
: undefined,
|
|
9983
10167
|
}));
|
|
9984
10168
|
}
|
|
@@ -10300,7 +10484,12 @@ export async function startLocalServer(opts) {
|
|
|
10300
10484
|
parameters,
|
|
10301
10485
|
question: typeof body.question === 'string' ? body.question : undefined,
|
|
10302
10486
|
});
|
|
10303
|
-
const contract = prepareBlockInvocation({
|
|
10487
|
+
const contract = prepareBlockInvocation({
|
|
10488
|
+
source,
|
|
10489
|
+
parameters,
|
|
10490
|
+
question: typeof body.question === 'string' ? body.question : undefined,
|
|
10491
|
+
surface: 'ask_ai',
|
|
10492
|
+
});
|
|
10304
10493
|
const certified = /\bstatus\s*=\s*"certified"/i.test(source);
|
|
10305
10494
|
const semantic = /\btype\s*=\s*"semantic"/i.test(source);
|
|
10306
10495
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -10396,6 +10585,7 @@ export async function startLocalServer(opts) {
|
|
|
10396
10585
|
parameters: body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
10397
10586
|
? body.parameters
|
|
10398
10587
|
: {},
|
|
10588
|
+
question: typeof body.question === 'string' ? body.question : undefined,
|
|
10399
10589
|
surface: 'notebook',
|
|
10400
10590
|
})
|
|
10401
10591
|
: null;
|
|
@@ -10414,15 +10604,31 @@ export async function startLocalServer(opts) {
|
|
|
10414
10604
|
tableMapping,
|
|
10415
10605
|
parameters: invocation?.values,
|
|
10416
10606
|
});
|
|
10417
|
-
|
|
10607
|
+
const semanticCompose = executableCell.type === 'dql'
|
|
10608
|
+
&& semanticLayer
|
|
10609
|
+
&& /\btype\s*=\s*"semantic"/i.test(executableCell.source)
|
|
10610
|
+
? await composeSemanticBlockSqlForRuntime(executableCell.source, semanticLayer, {
|
|
10611
|
+
driver: cellConnection.driver,
|
|
10612
|
+
tableMapping,
|
|
10613
|
+
parameters: invocation?.values,
|
|
10614
|
+
detectedProvider: semanticDetectedProvider,
|
|
10615
|
+
projectRoot,
|
|
10616
|
+
projectConfig,
|
|
10617
|
+
})
|
|
10618
|
+
: null;
|
|
10619
|
+
if (semanticCompose && !semanticCompose.sql) {
|
|
10620
|
+
throw new Error(semanticCompose.diagnostics.map((diagnostic) => diagnostic.message).join(' '));
|
|
10621
|
+
}
|
|
10622
|
+
const executableSql = semanticCompose?.sql ?? plan?.sql;
|
|
10623
|
+
if (!executableSql) {
|
|
10418
10624
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10419
10625
|
res.end(serializeJSON({ cellType: cell.type, result: null }));
|
|
10420
10626
|
return;
|
|
10421
10627
|
}
|
|
10422
|
-
const prepared = prepareLocalExecution(
|
|
10628
|
+
const prepared = prepareLocalExecution(executableSql, cellConnection, projectRoot, projectConfig);
|
|
10423
10629
|
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
10424
10630
|
assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
|
|
10425
|
-
const rawResult = await executor.executeQuery(prepared.sql, plan
|
|
10631
|
+
const rawResult = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), prepared.connection);
|
|
10426
10632
|
const normalized = normalizeQueryResult(rawResult);
|
|
10427
10633
|
// Enforce the block's declared invariants against the result set. This
|
|
10428
10634
|
// is additive: blocks without invariants produce `null` and the
|
|
@@ -10436,29 +10642,29 @@ export async function startLocalServer(opts) {
|
|
|
10436
10642
|
recordNotebookQueryRun(projectRoot, {
|
|
10437
10643
|
notebookPath: execContext.notebookPath,
|
|
10438
10644
|
cellId: execContext.cellId ?? cell.id,
|
|
10439
|
-
cellName: execContext.cellName ?? plan
|
|
10645
|
+
cellName: execContext.cellName ?? plan?.title ?? resolved.blockName,
|
|
10440
10646
|
researchRunId: execContext.researchRunId,
|
|
10441
10647
|
source: execContext.source ?? (cell.type === 'dql' ? 'notebook_dql_cell' : 'notebook_cell'),
|
|
10442
10648
|
status: 'success',
|
|
10443
10649
|
rowCount: normalized.rowCount ?? normalized.rows.length,
|
|
10444
10650
|
durationMs: Date.now() - start,
|
|
10445
|
-
sql:
|
|
10651
|
+
sql: executableSql,
|
|
10446
10652
|
objectKey: resolved.blockPath,
|
|
10447
10653
|
});
|
|
10448
10654
|
updateNotebookResearchFromCellExecution(projectRoot, execContext, {
|
|
10449
10655
|
status: 'success',
|
|
10450
10656
|
resultPreview: normalized,
|
|
10451
|
-
sql:
|
|
10657
|
+
sql: executableSql,
|
|
10452
10658
|
});
|
|
10453
10659
|
}
|
|
10454
10660
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10455
10661
|
res.end(serializeJSON({
|
|
10456
10662
|
cellType: cell.type,
|
|
10457
|
-
title: plan
|
|
10663
|
+
title: plan?.title,
|
|
10458
10664
|
blockName: resolved.blockName,
|
|
10459
10665
|
blockPath: resolved.blockPath,
|
|
10460
|
-
chartConfig: plan
|
|
10461
|
-
tests: plan
|
|
10666
|
+
chartConfig: plan?.chartConfig,
|
|
10667
|
+
tests: plan?.tests,
|
|
10462
10668
|
result: normalized,
|
|
10463
10669
|
...(invocation ? {
|
|
10464
10670
|
invocation: {
|
|
@@ -11391,9 +11597,34 @@ function latestOpenContextBootstrapSession(projectRoot) {
|
|
|
11391
11597
|
}
|
|
11392
11598
|
return null;
|
|
11393
11599
|
}
|
|
11394
|
-
|
|
11600
|
+
/**
|
|
11601
|
+
* API-004 / E2E-006: save-time compile for Block Studio. The manifest is
|
|
11602
|
+
* replaced atomically so lineage readers and agent retrieval never observe a
|
|
11603
|
+
* half-written snapshot, and an existing manifest survives compilation errors.
|
|
11604
|
+
*/
|
|
11605
|
+
export function compileBlockStudioManifest(projectRoot, projectConfig = loadProjectConfig(projectRoot)) {
|
|
11606
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot, projectConfig) ?? undefined;
|
|
11607
|
+
const manifest = buildManifest({ projectRoot, dqlVersion: 'notebook', dbtManifestPath });
|
|
11608
|
+
const manifestPath = join(projectRoot, 'dql-manifest.json');
|
|
11609
|
+
const tempPath = `${manifestPath}.tmp-${process.pid}-${Date.now()}`;
|
|
11610
|
+
try {
|
|
11611
|
+
writeFileSync(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
11612
|
+
renameSync(tempPath, manifestPath);
|
|
11613
|
+
}
|
|
11614
|
+
catch (error) {
|
|
11615
|
+
rmSync(tempPath, { force: true });
|
|
11616
|
+
throw error;
|
|
11617
|
+
}
|
|
11618
|
+
_lineageCache.delete(projectRoot);
|
|
11619
|
+
return manifest;
|
|
11620
|
+
}
|
|
11621
|
+
async function refreshLocalMetadataCatalog(projectRoot, manifest, semanticLayer) {
|
|
11395
11622
|
try {
|
|
11396
|
-
await ensureMetadataCatalogFresh(projectRoot, {
|
|
11623
|
+
await ensureMetadataCatalogFresh(projectRoot, {
|
|
11624
|
+
force: true,
|
|
11625
|
+
...(manifest ? { manifest } : {}),
|
|
11626
|
+
...(semanticLayer ? { semanticLayer } : {}),
|
|
11627
|
+
});
|
|
11397
11628
|
}
|
|
11398
11629
|
catch {
|
|
11399
11630
|
// The catalog is a rebuildable local cache. Save/certify flows should not
|
|
@@ -12846,13 +13077,13 @@ export function buildAgentPreviewSql(sql) {
|
|
|
12846
13077
|
* only when the owning entity is retained in GROUP BY. It never invents a
|
|
12847
13078
|
* relation, join key, or allocation rule.
|
|
12848
13079
|
*/
|
|
12849
|
-
export function repairExploratorySqlBeforeExecution(sql, schemaContext, question = '') {
|
|
13080
|
+
export function repairExploratorySqlBeforeExecution(sql, schemaContext, question = '', dialect = 'duckdb') {
|
|
12850
13081
|
const repairs = [];
|
|
12851
|
-
let repairedSql = qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs);
|
|
12852
|
-
repairedSql = repairExploratoryRelationQualifiers(repairedSql, repairs);
|
|
12853
|
-
repairedSql = repairExploratoryLifetimeMeasureSelection(repairedSql, schemaContext, question, repairs);
|
|
13082
|
+
let repairedSql = qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs, dialect);
|
|
13083
|
+
repairedSql = repairExploratoryRelationQualifiers(repairedSql, repairs, dialect);
|
|
13084
|
+
repairedSql = repairExploratoryLifetimeMeasureSelection(repairedSql, schemaContext, question, repairs, dialect);
|
|
12854
13085
|
repairedSql = repairExploratoryMisleadingPercentAliases(repairedSql, question, repairs);
|
|
12855
|
-
const grainRepair = repairExploratoryNonAdditiveAggregates(repairedSql, schemaContext, repairs);
|
|
13086
|
+
const grainRepair = repairExploratoryNonAdditiveAggregates(repairedSql, schemaContext, repairs, dialect);
|
|
12856
13087
|
repairedSql = grainRepair.sql;
|
|
12857
13088
|
return {
|
|
12858
13089
|
sql: repairedSql,
|
|
@@ -12903,8 +13134,8 @@ export function applyRequestedTopNToExploratorySql(sql, requestedTopN) {
|
|
|
12903
13134
|
}
|
|
12904
13135
|
return `${withoutTerminator}\nLIMIT ${requestedTopN}`;
|
|
12905
13136
|
}
|
|
12906
|
-
function qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs) {
|
|
12907
|
-
const analysis = analyzeSqlReferences(sql);
|
|
13137
|
+
function qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs, dialect = 'duckdb') {
|
|
13138
|
+
const analysis = analyzeSqlReferences(sql, dialect);
|
|
12908
13139
|
if (!analysis.parsed)
|
|
12909
13140
|
return sql;
|
|
12910
13141
|
let repaired = sql;
|
|
@@ -12932,8 +13163,8 @@ function qualifyExploratoryRelationsFromSchema(sql, schemaContext, repairs) {
|
|
|
12932
13163
|
}
|
|
12933
13164
|
return repaired;
|
|
12934
13165
|
}
|
|
12935
|
-
function repairExploratoryRelationQualifiers(sql, repairs) {
|
|
12936
|
-
const analysis = analyzeSqlReferences(sql);
|
|
13166
|
+
function repairExploratoryRelationQualifiers(sql, repairs, dialect = 'duckdb') {
|
|
13167
|
+
const analysis = analyzeSqlReferences(sql, dialect);
|
|
12937
13168
|
if (!analysis.parsed)
|
|
12938
13169
|
return sql;
|
|
12939
13170
|
const declared = Object.entries(analysis.aliasToRelation).map(([qualifier, relation]) => ({
|
|
@@ -12956,10 +13187,10 @@ function repairExploratoryRelationQualifiers(sql, repairs) {
|
|
|
12956
13187
|
}
|
|
12957
13188
|
return repaired;
|
|
12958
13189
|
}
|
|
12959
|
-
function repairExploratoryLifetimeMeasureSelection(sql, schemaContext, question, repairs) {
|
|
13190
|
+
function repairExploratoryLifetimeMeasureSelection(sql, schemaContext, question, repairs, dialect = 'duckdb') {
|
|
12960
13191
|
if (!/\b(lifetime|life\s*span|lifespan|customer\s+life)\b/i.test(question))
|
|
12961
13192
|
return sql;
|
|
12962
|
-
const analysis = analyzeSqlReferences(sql);
|
|
13193
|
+
const analysis = analyzeSqlReferences(sql, dialect);
|
|
12963
13194
|
if (!analysis.parsed || analysis.joins.length === 0)
|
|
12964
13195
|
return sql;
|
|
12965
13196
|
const groupClause = sql.match(/\bgroup\s+by\s+([\s\S]*?)(?:\border\s+by\b|\blimit\b|\bqualify\b|\bhaving\b|$)/i)?.[1] ?? '';
|
|
@@ -12985,8 +13216,8 @@ function repairExploratoryLifetimeMeasureSelection(sql, schemaContext, question,
|
|
|
12985
13216
|
}
|
|
12986
13217
|
return repaired;
|
|
12987
13218
|
}
|
|
12988
|
-
function repairExploratoryNonAdditiveAggregates(sql, schemaContext, repairs) {
|
|
12989
|
-
const analysis = analyzeSqlReferences(sql);
|
|
13219
|
+
function repairExploratoryNonAdditiveAggregates(sql, schemaContext, repairs, dialect = 'duckdb') {
|
|
13220
|
+
const analysis = analyzeSqlReferences(sql, dialect);
|
|
12990
13221
|
if (!analysis.parsed || analysis.joins.length === 0)
|
|
12991
13222
|
return { sql };
|
|
12992
13223
|
const risky = analysis.aggregates.filter((aggregate) => aggregate.func.toLowerCase() === 'sum'
|
|
@@ -13108,6 +13339,16 @@ export function buildExploratoryJoinProbeSql(input) {
|
|
|
13108
13339
|
SELECT right_key, COUNT(*) AS match_count
|
|
13109
13340
|
FROM matches
|
|
13110
13341
|
GROUP BY right_key
|
|
13342
|
+
),
|
|
13343
|
+
left_key_counts AS (
|
|
13344
|
+
SELECT join_key, COUNT(*) AS key_count
|
|
13345
|
+
FROM left_sample
|
|
13346
|
+
GROUP BY join_key
|
|
13347
|
+
),
|
|
13348
|
+
right_key_counts AS (
|
|
13349
|
+
SELECT join_key, COUNT(*) AS key_count
|
|
13350
|
+
FROM right_sample
|
|
13351
|
+
GROUP BY join_key
|
|
13111
13352
|
)
|
|
13112
13353
|
SELECT
|
|
13113
13354
|
(SELECT COUNT(*) FROM left_sample) AS left_sample_rows,
|
|
@@ -13116,7 +13357,87 @@ SELECT
|
|
|
13116
13357
|
(SELECT COUNT(*) FROM left_sample AS l WHERE NOT EXISTS (SELECT 1 FROM right_sample AS r WHERE r.join_key = l.join_key)) AS unmatched_left_sample_rows,
|
|
13117
13358
|
(SELECT COUNT(*) FROM right_sample AS r WHERE NOT EXISTS (SELECT 1 FROM left_sample AS l WHERE l.join_key = r.join_key)) AS unmatched_right_sample_rows,
|
|
13118
13359
|
(SELECT COALESCE(MAX(match_count), 0) FROM left_counts) AS max_matches_per_left_key,
|
|
13119
|
-
(SELECT COALESCE(MAX(match_count), 0) FROM right_counts) AS max_matches_per_right_key
|
|
13360
|
+
(SELECT COALESCE(MAX(match_count), 0) FROM right_counts) AS max_matches_per_right_key,
|
|
13361
|
+
(SELECT COALESCE(MAX(key_count), 0) FROM left_key_counts) AS max_left_rows_per_key,
|
|
13362
|
+
(SELECT COALESCE(MAX(key_count), 0) FROM right_key_counts) AS max_right_rows_per_key`;
|
|
13363
|
+
}
|
|
13364
|
+
/**
|
|
13365
|
+
* CTE names are query-internal derived relations, not warehouse tables. A join
|
|
13366
|
+
* whose endpoint is a CTE (e.g. `WITH joy_items AS (…) … JOIN joy_items`) must
|
|
13367
|
+
* be excluded from declared-path enforcement and from join probes — probing
|
|
13368
|
+
* `FROM "joy_items"` throws a DuckDB catalog error for a table that was never
|
|
13369
|
+
* supposed to exist. The physical joins INSIDE the CTE are still present in
|
|
13370
|
+
* the parsed join list and get validated/probed on their own.
|
|
13371
|
+
*/
|
|
13372
|
+
export function probeableExploratoryJoins(joins, ctes) {
|
|
13373
|
+
const cteNames = new Set(ctes.map((name) => name.split('.').at(-1)?.toLowerCase() ?? name.toLowerCase()));
|
|
13374
|
+
if (cteNames.size === 0)
|
|
13375
|
+
return joins;
|
|
13376
|
+
return joins.filter((join) => ![join.leftRelation, join.rightRelation].some((relation) => {
|
|
13377
|
+
const bare = relation?.replace(/["`\[\]]/g, '').split('.').at(-1)?.toLowerCase();
|
|
13378
|
+
return Boolean(bare && cteNames.has(bare));
|
|
13379
|
+
}));
|
|
13380
|
+
}
|
|
13381
|
+
function declaredExploratoryRelationMatches(sqlRelation, declaredRelation) {
|
|
13382
|
+
if (!declaredRelation)
|
|
13383
|
+
return false;
|
|
13384
|
+
const normalize = (value) => value.replace(/["`\[\]]/g, '').trim();
|
|
13385
|
+
return exploratoryRelationsMatch(normalize(sqlRelation), normalize(declaredRelation));
|
|
13386
|
+
}
|
|
13387
|
+
function exploratoryColumnsMatch(sqlColumn, declaredColumn) {
|
|
13388
|
+
const normalize = (value) => value.replace(/["`\[\]]/g, '').split('.').at(-1)?.trim().toLowerCase() ?? value.toLowerCase();
|
|
13389
|
+
return normalize(sqlColumn) === normalize(declaredColumn);
|
|
13390
|
+
}
|
|
13391
|
+
/**
|
|
13392
|
+
* Gate a join probe against the declared relationship. Returns an error string
|
|
13393
|
+
* when the UNFILTERED key samples structurally contradict the declaration:
|
|
13394
|
+
* - zero key overlap while both sides have rows (wrong/mistyped key), or
|
|
13395
|
+
* - duplicate keys on the declared "one" side of a *_to_one / one_to_* edge.
|
|
13396
|
+
* Sampling caveat: gates only fire on definitive contradictions, never on low
|
|
13397
|
+
* match rates, so a sparse but real relationship still executes.
|
|
13398
|
+
*/
|
|
13399
|
+
export function exploratoryProbeContradiction(rows, join, edge) {
|
|
13400
|
+
const row = Array.isArray(rows) && rows[0] && typeof rows[0] === 'object'
|
|
13401
|
+
? rows[0]
|
|
13402
|
+
: {};
|
|
13403
|
+
const number = (key) => {
|
|
13404
|
+
const value = Number(row[key]);
|
|
13405
|
+
return Number.isFinite(value) ? value : 0;
|
|
13406
|
+
};
|
|
13407
|
+
const leftRows = number('left_sample_rows');
|
|
13408
|
+
const rightRows = number('right_sample_rows');
|
|
13409
|
+
const joined = number('joined_rows');
|
|
13410
|
+
if (leftRows > 0 && rightRows > 0 && joined === 0) {
|
|
13411
|
+
return `The join key ${join.leftColumn} = ${join.rightColumn} produced no matching rows between ${join.leftRelation} and ${join.rightRelation} (unfiltered sample). The declared key pair is likely wrong — review the relationship before executing this analysis.`;
|
|
13412
|
+
}
|
|
13413
|
+
if (!edge)
|
|
13414
|
+
return undefined;
|
|
13415
|
+
// Orient the declared cardinality onto the parsed join: which physical side is
|
|
13416
|
+
// the declared "one" side? Duplicated keys there contradict the declaration.
|
|
13417
|
+
const leftIsFrom = Boolean(join.leftRelation && declaredExploratoryRelationMatches(join.leftRelation, edge.fromRelation));
|
|
13418
|
+
const rightIsFrom = Boolean(join.rightRelation && declaredExploratoryRelationMatches(join.rightRelation, edge.fromRelation));
|
|
13419
|
+
if (leftIsFrom === rightIsFrom)
|
|
13420
|
+
return undefined; // orientation unresolved — do not guess
|
|
13421
|
+
const oneSide = edge.cardinality === 'many_to_one' ? 'to' : edge.cardinality === 'one_to_many' ? 'from' : edge.cardinality === 'one_to_one' ? 'both' : undefined;
|
|
13422
|
+
if (!oneSide)
|
|
13423
|
+
return undefined;
|
|
13424
|
+
// Per-side key duplication measured WITHIN each sample (max rows sharing one
|
|
13425
|
+
// key value). Duplicates inside a sample are definitive — a subset of a table
|
|
13426
|
+
// cannot show duplicates the full table does not have. The pairwise
|
|
13427
|
+
// max_matches_* columns are NOT used here: they inflate whenever the
|
|
13428
|
+
// legitimate "many" side repeats a key value.
|
|
13429
|
+
const leftDuplicated = number('max_left_rows_per_key') > 1;
|
|
13430
|
+
const rightDuplicated = number('max_right_rows_per_key') > 1;
|
|
13431
|
+
const fromSideIsLeft = leftIsFrom;
|
|
13432
|
+
const duplicatedSides = [
|
|
13433
|
+
...(leftDuplicated ? [fromSideIsLeft ? 'from' : 'to'] : []),
|
|
13434
|
+
...(rightDuplicated ? [fromSideIsLeft ? 'to' : 'from'] : []),
|
|
13435
|
+
];
|
|
13436
|
+
const violated = oneSide === 'both' ? duplicatedSides.length > 0 : duplicatedSides.includes(oneSide);
|
|
13437
|
+
if (violated) {
|
|
13438
|
+
return `Relationship "${edge.relationshipId}" declares ${edge.cardinality}, but the sampled join keys show duplicates on the declared unique side. Executing would duplicate rows — validate and correct the relationship first.`;
|
|
13439
|
+
}
|
|
13440
|
+
return undefined;
|
|
13120
13441
|
}
|
|
13121
13442
|
function summarizeExploratoryJoinProbe(rows, leftRelation, leftColumn, rightRelation, rightColumn) {
|
|
13122
13443
|
const row = Array.isArray(rows) && rows[0] && typeof rows[0] === 'object'
|
|
@@ -13716,28 +14037,6 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
|
|
|
13716
14037
|
validation: validateBlockStudioSource(source, semanticLayer),
|
|
13717
14038
|
};
|
|
13718
14039
|
}
|
|
13719
|
-
function semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver) {
|
|
13720
|
-
if (provider === 'dbt' && metricFlowReady) {
|
|
13721
|
-
return { status: 'ready', engine: 'metricflow', reason: null };
|
|
13722
|
-
}
|
|
13723
|
-
const native = semanticLayer.composeQuery({ metrics: [metricName], dimensions: [], driver });
|
|
13724
|
-
if (native)
|
|
13725
|
-
return { status: 'ready', engine: 'native', reason: null };
|
|
13726
|
-
const metric = semanticLayer.getMetric(metricName);
|
|
13727
|
-
if (provider === 'dbt') {
|
|
13728
|
-
const kind = metric?.metricType || metric?.aggregation || metric?.type || 'metric';
|
|
13729
|
-
return {
|
|
13730
|
-
status: 'requires_setup',
|
|
13731
|
-
engine: null,
|
|
13732
|
-
reason: `${kind} metric requires MetricFlow execution. Install/configure the MetricFlow CLI, then refresh the semantic layer.`,
|
|
13733
|
-
};
|
|
13734
|
-
}
|
|
13735
|
-
return {
|
|
13736
|
-
status: 'unsupported',
|
|
13737
|
-
engine: null,
|
|
13738
|
-
reason: 'The metric does not have enough composable measure and relation metadata.',
|
|
13739
|
-
};
|
|
13740
|
-
}
|
|
13741
14040
|
function parseBlockStudioArrayField(source, key) {
|
|
13742
14041
|
const match = source.match(new RegExp(`\\b${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'i'));
|
|
13743
14042
|
if (!match)
|
|
@@ -13790,8 +14089,7 @@ export async function resolveSemanticTableMapping(executor, connection, semantic
|
|
|
13790
14089
|
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
|
|
13791
14090
|
FROM information_schema.tables
|
|
13792
14091
|
WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')
|
|
13793
|
-
ORDER BY table_schema, table_name
|
|
13794
|
-
LIMIT 2000`, [], {}, connection);
|
|
14092
|
+
ORDER BY table_schema, table_name`, [], {}, connection);
|
|
13795
14093
|
return buildSemanticTableMapping(semanticLayer, tablesResult.rows);
|
|
13796
14094
|
}
|
|
13797
14095
|
catch {
|
|
@@ -13833,41 +14131,17 @@ function isDbtSemanticRuntime(projectConfig, detectedProvider, semanticLayer) {
|
|
|
13833
14131
|
return true;
|
|
13834
14132
|
return Boolean(semanticLayer?.listMetrics().some((metric) => metric.source?.provider === 'dbt'));
|
|
13835
14133
|
}
|
|
13836
|
-
function composeRuntimeSemanticQuery(request, semanticLayer, context) {
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
dbtProjectPath,
|
|
13848
|
-
metrics: request.metrics,
|
|
13849
|
-
dimensions: request.dimensions,
|
|
13850
|
-
filters: request.filters,
|
|
13851
|
-
timeDimension: request.timeDimension,
|
|
13852
|
-
orderBy: request.orderBy,
|
|
13853
|
-
limit: request.limit,
|
|
13854
|
-
savedQuery: request.savedQuery,
|
|
13855
|
-
});
|
|
13856
|
-
return {
|
|
13857
|
-
sql: compiled.sql,
|
|
13858
|
-
joins: [],
|
|
13859
|
-
tables: [],
|
|
13860
|
-
engine: 'metricflow',
|
|
13861
|
-
};
|
|
13862
|
-
}
|
|
13863
|
-
catch (error) {
|
|
13864
|
-
// An explicit MetricFlow request must retain its strict engine contract.
|
|
13865
|
-
// For the default dbt path, a missing local CLI may fall back only if the
|
|
13866
|
-
// native composer can safely materialize the requested simple metrics.
|
|
13867
|
-
if (request.engine === 'metricflow' || !(error instanceof MetricFlowUnavailableError))
|
|
13868
|
-
throw error;
|
|
13869
|
-
}
|
|
13870
|
-
}
|
|
14134
|
+
async function composeRuntimeSemanticQuery(request, semanticLayer, context) {
|
|
14135
|
+
return compileSemanticRuntimeQuery(request, {
|
|
14136
|
+
projectRoot: context.projectRoot,
|
|
14137
|
+
projectConfig: context.projectConfig,
|
|
14138
|
+
detectedProvider: context.detectedProvider,
|
|
14139
|
+
semanticLayer,
|
|
14140
|
+
driver: context.driver,
|
|
14141
|
+
tableMapping: context.tableMapping,
|
|
14142
|
+
});
|
|
14143
|
+
}
|
|
14144
|
+
function composeRuntimeSemanticQueryNative(request, semanticLayer, context) {
|
|
13871
14145
|
const composed = semanticLayer.composeQuery({
|
|
13872
14146
|
metrics: request.metrics,
|
|
13873
14147
|
dimensions: request.dimensions,
|
|
@@ -13936,7 +14210,7 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
13936
14210
|
let composed;
|
|
13937
14211
|
try {
|
|
13938
14212
|
composed = options?.projectRoot && options.projectConfig
|
|
13939
|
-
?
|
|
14213
|
+
? composeRuntimeSemanticQueryNative({
|
|
13940
14214
|
metrics,
|
|
13941
14215
|
dimensions: config.dimensions,
|
|
13942
14216
|
filters: mergeSemanticRuntimeFilters(config.filters, runtimeBindings.filters),
|
|
@@ -13945,9 +14219,6 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
13945
14219
|
: undefined,
|
|
13946
14220
|
limit: runtimeBindings.limit ?? config.limit,
|
|
13947
14221
|
}, semanticLayer, {
|
|
13948
|
-
projectRoot: options.projectRoot,
|
|
13949
|
-
projectConfig: options.projectConfig,
|
|
13950
|
-
detectedProvider: options.detectedProvider ?? null,
|
|
13951
14222
|
driver: options.driver,
|
|
13952
14223
|
tableMapping: options.tableMapping,
|
|
13953
14224
|
})
|
|
@@ -13973,10 +14244,14 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
13973
14244
|
}
|
|
13974
14245
|
if (!composed) {
|
|
13975
14246
|
const provider = options?.projectConfig && isDbtSemanticRuntime(options.projectConfig, options.detectedProvider, semanticLayer) ? 'dbt' : (options?.detectedProvider ?? 'dql');
|
|
13976
|
-
const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
|
|
13977
14247
|
const reasons = metrics.map((metricName) => {
|
|
13978
|
-
|
|
13979
|
-
|
|
14248
|
+
if (semanticLayer.canComposeMetric(metricName))
|
|
14249
|
+
return null;
|
|
14250
|
+
const metric = semanticLayer.getMetric(metricName);
|
|
14251
|
+
const kind = metric?.metricType || metric?.aggregation || metric?.type || 'metric';
|
|
14252
|
+
return provider === 'dbt'
|
|
14253
|
+
? `${metricName}: ${kind} metric requires a configured dbt Cloud or local MetricFlow runtime.`
|
|
14254
|
+
: `${metricName}: the metric does not have enough composable measure and relation metadata.`;
|
|
13980
14255
|
}).filter((reason) => Boolean(reason));
|
|
13981
14256
|
diagnostics.push({
|
|
13982
14257
|
severity: 'error',
|
|
@@ -13993,6 +14268,61 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
13993
14268
|
semanticRefs,
|
|
13994
14269
|
};
|
|
13995
14270
|
}
|
|
14271
|
+
/**
|
|
14272
|
+
* Runtime-aware semantic block compiler. Static validation remains synchronous
|
|
14273
|
+
* and native-only, while execution surfaces can use the bundled dbt Cloud or
|
|
14274
|
+
* local MetricFlow adapters without changing the persisted semantic identities.
|
|
14275
|
+
*/
|
|
14276
|
+
async function composeSemanticBlockSqlForRuntime(source, semanticLayer, options) {
|
|
14277
|
+
const native = composeSemanticBlockSql(source, semanticLayer, options);
|
|
14278
|
+
if (native.sql)
|
|
14279
|
+
return native;
|
|
14280
|
+
const config = parseSemanticBlockConfig(source);
|
|
14281
|
+
if (config.blockType !== 'semantic')
|
|
14282
|
+
return native;
|
|
14283
|
+
const metrics = config.metrics.length > 0 ? config.metrics : config.metric ? [config.metric] : [];
|
|
14284
|
+
if (metrics.length === 0 || native.diagnostics.some((diagnostic) => diagnostic.code === 'semantic_ref'))
|
|
14285
|
+
return native;
|
|
14286
|
+
const runtimeBindings = semanticRuntimeParameterBindings(source, options.parameters ?? {});
|
|
14287
|
+
try {
|
|
14288
|
+
const compiled = await composeRuntimeSemanticQuery({
|
|
14289
|
+
metrics,
|
|
14290
|
+
dimensions: config.dimensions,
|
|
14291
|
+
filters: mergeSemanticRuntimeFilters(config.filters, runtimeBindings.filters),
|
|
14292
|
+
timeDimension: config.timeDimension && config.granularity
|
|
14293
|
+
? { name: config.timeDimension, granularity: config.granularity }
|
|
14294
|
+
: undefined,
|
|
14295
|
+
limit: runtimeBindings.limit ?? config.limit,
|
|
14296
|
+
}, semanticLayer, {
|
|
14297
|
+
projectRoot: options.projectRoot,
|
|
14298
|
+
projectConfig: options.projectConfig,
|
|
14299
|
+
detectedProvider: options.detectedProvider ?? null,
|
|
14300
|
+
driver: options.driver,
|
|
14301
|
+
tableMapping: options.tableMapping,
|
|
14302
|
+
});
|
|
14303
|
+
if (!compiled)
|
|
14304
|
+
return native;
|
|
14305
|
+
return {
|
|
14306
|
+
sql: compiled.sql,
|
|
14307
|
+
diagnostics: native.diagnostics.filter((diagnostic) => diagnostic.code !== 'semantic_compose_failed'),
|
|
14308
|
+
semanticRefs: native.semanticRefs,
|
|
14309
|
+
};
|
|
14310
|
+
}
|
|
14311
|
+
catch (error) {
|
|
14312
|
+
return {
|
|
14313
|
+
sql: null,
|
|
14314
|
+
diagnostics: [
|
|
14315
|
+
...native.diagnostics.filter((diagnostic) => diagnostic.code !== 'semantic_compose_failed'),
|
|
14316
|
+
{
|
|
14317
|
+
severity: 'error',
|
|
14318
|
+
code: isSemanticRuntimeError(error) ? 'semantic_runtime_required' : 'semantic_compose_failed',
|
|
14319
|
+
message: error instanceof Error ? error.message : String(error),
|
|
14320
|
+
},
|
|
14321
|
+
],
|
|
14322
|
+
semanticRefs: native.semanticRefs,
|
|
14323
|
+
};
|
|
14324
|
+
}
|
|
14325
|
+
}
|
|
13996
14326
|
function semanticRuntimeParameterBindings(source, values) {
|
|
13997
14327
|
const program = new Parser(source, '<semantic-parameters>').parse();
|
|
13998
14328
|
const block = program.statements.find((statement) => statement.kind === NodeKind.BlockDecl);
|