@duckcodeailabs/dql-cli 1.8.6 → 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.
@@ -19,7 +19,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
19
19
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
20
20
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
21
21
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
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';
22
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, aggregationIntegrityIssuesForSql, 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';
23
23
  import { gatherProposeEnrichment } from './propose-enrich.js';
24
24
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
25
25
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -1186,7 +1186,8 @@ export async function startLocalServer(opts) {
1186
1186
  ? { preferredExecutionId: routeDecision.meaningResolution.recommendedExecutionId }
1187
1187
  : {}),
1188
1188
  executeCertifiedBlock: executeCertifiedBlockForAgent,
1189
- executeGeneratedSql: executeGeneratedSqlForAgent,
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
- * PRD-001 / AGT-002 / AGT-006 / EXP-002every answer surface receives the
1301
- * same executable DQL artifact. Generated SQL is parameterized before the
1302
- * artifact leaves the runtime; it remains compiled evidence, not the primary
1303
- * user-facing object. Saving later persists this exact source without another
1304
- * model call.
1336
+ * AGT-010 / API-003 / UI-011attach 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
- let artifact = governedAnswer.dqlArtifact;
1309
- if ((!artifact || artifact.kind === 'sql_block') && originalSql) {
1310
- const parameterized = parameterizeSqlForDqlImport(originalSql);
1311
- const artifactName = artifact?.name
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(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
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: prepared.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
- return executeDqlArtifactSourceForAgent(source, {
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
- const executeGeneratedSqlForAgent = async (sql) => {
2638
- const activeConnection = requireActiveConnection();
2639
- const boundedSql = buildAgentPreviewSql(sql);
2640
- const semantic = prepareSemanticSql(boundedSql, semanticLayer);
2641
- if (semantic.unresolvedRefs.length > 0) {
2642
- throw new Error(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`);
2643
- }
2644
- const prepared = prepareLocalExecution(semantic.sql, activeConnection, projectRoot, projectConfig);
2645
- const app = loadRuntimeApp(projectRoot, activePersonaAppId());
2646
- assertAppAccess({ app, domain: app?.domain, level: 'execute' });
2647
- const rawResult = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
2648
- const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
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
- columns: normalized.columns,
2651
- rows: normalized.rows,
2652
- rowCount: normalized.rowCount,
2653
- executionTime: normalized.executionTime,
2654
- sql: prepared.sql,
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 executeGeneratedSqlForAgent(boundedSql);
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 200`;
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.
@@ -13140,10 +13231,14 @@ export function repairExploratorySqlBeforeExecution(sql, schemaContext, question
13140
13231
  repairedSql = repairExploratoryMisleadingPercentAliases(repairedSql, question, repairs);
13141
13232
  const grainRepair = repairExploratoryNonAdditiveAggregates(repairedSql, schemaContext, repairs, dialect);
13142
13233
  repairedSql = grainRepair.sql;
13234
+ const numericIssue = aggregationIntegrityIssuesForSql(repairedSql, [], dialect)
13235
+ .find((issue) => issue.kind === 'premature_rounding' || issue.kind === 'lossy_numeric_cast');
13143
13236
  return {
13144
13237
  sql: repairedSql,
13145
13238
  repairs,
13146
- ...(grainRepair.blockedReason ? { blockedReason: grainRepair.blockedReason } : {}),
13239
+ ...(grainRepair.blockedReason || numericIssue
13240
+ ? { blockedReason: grainRepair.blockedReason ?? numericIssue.message }
13241
+ : {}),
13147
13242
  };
13148
13243
  }
13149
13244
  /**
@@ -13417,20 +13512,21 @@ SELECT
13417
13512
  (SELECT COALESCE(MAX(key_count), 0) FROM right_key_counts) AS max_right_rows_per_key`;
13418
13513
  }
13419
13514
  /**
13420
- * CTE names are query-internal derived relations, not warehouse tables. A join
13421
- * whose endpoint is a CTE (e.g. `WITH joy_items AS (…) JOIN joy_items`) must
13422
- * be excluded from declared-path enforcement and from join probes — probing
13423
- * `FROM "joy_items"` throws a DuckDB catalog error for a table that was never
13424
- * supposed to exist. The physical joins INSIDE the CTE are still present in
13425
- * the parsed join list and get validated/probed on their own.
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.
13426
13521
  */
13427
- export function probeableExploratoryJoins(joins, ctes) {
13428
- const cteNames = new Set(ctes.map((name) => name.split('.').at(-1)?.toLowerCase() ?? name.toLowerCase()));
13429
- if (cteNames.size === 0)
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)
13430
13526
  return joins;
13431
13527
  return joins.filter((join) => ![join.leftRelation, join.rightRelation].some((relation) => {
13432
13528
  const bare = relation?.replace(/["`\[\]]/g, '').split('.').at(-1)?.toLowerCase();
13433
- return Boolean(bare && cteNames.has(bare));
13529
+ return Boolean(bare && internalNames.has(bare));
13434
13530
  }));
13435
13531
  }
13436
13532
  function declaredExploratoryRelationMatches(sqlRelation, declaredRelation) {