@duckcodeailabs/dql-cli 1.12.10 → 1.12.11
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/{AgentAnswerCard-Dp4odOLE.js → AgentAnswerCard-CpL2kDCV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-uvVs284-.js → AgentLogPage-hzHqpD3-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-DEUS19WF.js → AiBuildDialog-BQRWoxp9.js} +1 -1
- package/dist/assets/dql-notebook/assets/AiBuildResult-D9t_4sQl.js +33 -0
- package/dist/assets/dql-notebook/assets/{AiSidePanel-C7BZVSkM.js → AiSidePanel-BCQB9rVF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-4TgtH6SB.js → AnalyticsHome-Ba4Ve_Gd.js} +1 -1
- package/dist/assets/dql-notebook/assets/AppsView-CdAT9BOq.js +4466 -0
- package/dist/assets/dql-notebook/assets/{BlockStudio-DUXN3MKz.js → BlockStudio-Brg4fwn5.js} +13 -13
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-D8FIgrkF.js → BusinessArtifactView-CnTbFVGT.js} +1 -1
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-CQe2FUZq.js +22 -0
- package/dist/assets/dql-notebook/assets/{GitPage-BS2sm9zh.js → GitPage-DHyDBChc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-B_MZsB0z.js → GlobalAiRail-Cys4n_rq.js} +1 -1
- package/dist/assets/dql-notebook/assets/GovernedContextPage-DyKoDSZ2.js +7 -0
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-Bc5pLakt.js → HelpDocsPage-CL0vbSER.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HomePage-DQSo-K4y.js → HomePage-DzAucwBW.js} +9 -4
- package/dist/assets/dql-notebook/assets/{LineageDAG-RdpWmGmR.js → LineageDAG-QHqd_AM5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-CZEx2r1H.js → LineageDetailView-DdVH-nz6.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-P7ZdOW8-.js → LineageDrawer-BvrVZNKY.js} +1 -1
- package/dist/assets/dql-notebook/assets/LineagePathBreadcrumb-CSut-zvm.js +14 -0
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-Ctp_yFh8.js → MiniLineageGraph-zg_1wW3M.js} +1 -1
- package/dist/assets/dql-notebook/assets/NewBlockModal-9cDletl-.js +1 -0
- package/dist/assets/dql-notebook/assets/NewNotebookModal-V6de8L4d.js +46 -0
- package/dist/assets/dql-notebook/assets/NotebookEditor-EZ0pfWVs.js +200 -0
- package/dist/assets/dql-notebook/assets/{ReadinessPage-C8hATaT_.js → ReadinessPage-BaGvjyfd.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-DX2f53J2.js → SetupOnboarding-Bg-MjbC9.js} +2 -2
- package/dist/assets/dql-notebook/assets/{TrustBadge-DPnKzRSY.js → TrustBadge-DGuHB8kP.js} +1 -1
- package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-CwxQ4T19.js → UnifiedAgentRunPanel-BlWAVEwO.js} +17 -17
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-Bm9oByWA.js → answer-to-notebook-BDzY4MuQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-D2FbLWqh.js → arrow-left-IjlnAihE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-OYItAZ-s.js → arrow-right-BI4cXgOY.js} +2 -2
- package/dist/assets/dql-notebook/assets/{book-open-text-BnnirfAc.js → book-open-text-i6xhxu_I.js} +1 -1
- package/dist/assets/dql-notebook/assets/{bot-BJNxPYFm.js → bot-C6DE7Q6m.js} +1 -1
- package/dist/assets/dql-notebook/assets/{chevron-up-DM5LX-26.js → chevron-up-DSEILnDs.js} +2 -2
- package/dist/assets/dql-notebook/assets/{circle-x-Dic_-4tF.js → circle-x-BoAorVSm.js} +1 -1
- package/dist/assets/dql-notebook/assets/{codemirror-1_I9gm6Z.js → codemirror-DXt7RW72.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-C9zxjHHX.js → dagre.esm-CycymVj6.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-DHxAcoCE.js → external-link-ZwE5iHsX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-02I-EOjL.js → grip-vertical-Dnp2st8l.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-BTuliKhK.js +634 -0
- package/dist/assets/dql-notebook/assets/{link-2-Delh-lai.js → link-2-CTKe6Snr.js} +2 -2
- package/dist/assets/dql-notebook/assets/{list-tree-BrIZg6l6.js → list-tree-Ced8e_S1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{maximize-2-CQOA-bbN.js → maximize-2-Cojc6867.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CUDBPkwe.js → minimize-2-CBIAE0XH.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-CwhZfkKJ.js → panel-right-open-po3bWSeL.js} +2 -2
- package/dist/assets/dql-notebook/assets/{pencil-Cf6-MgpT.js → pencil-Cb45c6WU.js} +2 -2
- package/dist/assets/dql-notebook/assets/{rotate-ccw-DndbJAiz.js → rotate-ccw-B9v1LTJ0.js} +2 -2
- package/dist/assets/dql-notebook/assets/{semantic-fields-DwKPY_mU.js → semantic-fields-B6ii0an0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-DvcMC9wZ.js → star-CO4tfaDq.js} +2 -2
- package/dist/assets/dql-notebook/assets/{triangle-alert-DUf4Ook0.js → triangle-alert-BSdIu2El.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DbdBi0oH.js → usePersistedAgentThreadId-CkVFrrjk.js} +2 -2
- package/dist/assets/dql-notebook/assets/{user-round-DSt0xU3K.js → user-round-BfrV8U-W.js} +3 -3
- package/dist/assets/dql-notebook/assets/{wand-sparkles-ro8Cq0ro.js → wand-sparkles-BgTxZNBc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-DVJXehjU.js → workflow--2ycp0yy.js} +2 -2
- package/dist/assets/dql-notebook/assets/{wrench-DxUnoHtz.js → wrench-LMCrpa9_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-9Gtb3EAd.js → x-CeNgQwGm.js} +2 -2
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/local-runtime.d.ts +40 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +602 -172
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AiBuildResult-B9KmBRsh.js +0 -33
- package/dist/assets/dql-notebook/assets/AppsView-CnfNtk5W.js +0 -4466
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-DR8hTZ95.js +0 -22
- package/dist/assets/dql-notebook/assets/GovernedContextPage-CNM5UCAH.js +0 -7
- package/dist/assets/dql-notebook/assets/LineagePathBreadcrumb-DTUMFkJ8.js +0 -14
- package/dist/assets/dql-notebook/assets/NewBlockModal-BLtjuWGy.js +0 -1
- package/dist/assets/dql-notebook/assets/NewNotebookModal-xgaKB2kI.js +0 -46
- package/dist/assets/dql-notebook/assets/NotebookEditor-D6RgLQw-.js +0 -202
- package/dist/assets/dql-notebook/assets/index-Ca3UeUWI.js +0 -644
package/dist/local-runtime.js
CHANGED
|
@@ -3474,7 +3474,7 @@ export async function startLocalServer(opts) {
|
|
|
3474
3474
|
* string, so the only remaining reasons to differ are the connection and the
|
|
3475
3475
|
* governance gates, both of which report themselves honestly.
|
|
3476
3476
|
*/
|
|
3477
|
-
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection) => {
|
|
3477
|
+
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings) => {
|
|
3478
3478
|
const activeConnection = requireActiveConnection(executionConnection);
|
|
3479
3479
|
const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
|
|
3480
3480
|
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
@@ -3535,7 +3535,7 @@ export async function startLocalServer(opts) {
|
|
|
3535
3535
|
})
|
|
3536
3536
|
: null;
|
|
3537
3537
|
const rawResult = semanticExecution?.result
|
|
3538
|
-
?? await executor.executeQuery(preparation.executedSql, [], runtimeVariables({}), preparation.connection);
|
|
3538
|
+
?? await executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
|
|
3539
3539
|
const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
|
|
3540
3540
|
// The authoritative bound. `buildRowBoundedSql` may legitimately decline to
|
|
3541
3541
|
// touch the statement, and only one of 15 drivers honours a maxRows hint, so
|
|
@@ -3803,6 +3803,115 @@ export async function startLocalServer(opts) {
|
|
|
3803
3803
|
return [];
|
|
3804
3804
|
}
|
|
3805
3805
|
};
|
|
3806
|
+
const boundedRepairError = (message, status, warehouseFailure) => Object.assign(new Error(message), { status, ...(warehouseFailure ? { warehouseFailure } : {}) });
|
|
3807
|
+
/**
|
|
3808
|
+
* Shared one-attempt SQL repair for Ask and Notebook. It never creates a
|
|
3809
|
+
* conversation, writes a DQL file, changes targets, or promotes trust.
|
|
3810
|
+
*/
|
|
3811
|
+
const executeBoundedSqlRepair = async (input) => {
|
|
3812
|
+
let result;
|
|
3813
|
+
let repairedSql = input.sourceSql;
|
|
3814
|
+
let repairMode = 'deterministic';
|
|
3815
|
+
let firstFailure;
|
|
3816
|
+
try {
|
|
3817
|
+
result = await executeGeneratedSqlDirect(input.question, input.sourceSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables });
|
|
3818
|
+
}
|
|
3819
|
+
catch (error) {
|
|
3820
|
+
firstFailure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
3821
|
+
}
|
|
3822
|
+
if (!result) {
|
|
3823
|
+
if (firstFailure?.retryDisposition !== 'model_repair') {
|
|
3824
|
+
throw boundedRepairError(firstFailure?.redactedMessage ?? 'The deterministic repair did not produce an executable query.', 422, firstFailure);
|
|
3825
|
+
}
|
|
3826
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
3827
|
+
if (!provider) {
|
|
3828
|
+
throw boundedRepairError('Configure an available AI provider in Settings to repair this query.', 409);
|
|
3829
|
+
}
|
|
3830
|
+
let schemaContext;
|
|
3831
|
+
try {
|
|
3832
|
+
schemaContext = await getSchemaContextForAgent(input.question, undefined, input.targetConnection, input.targetConnectionName);
|
|
3833
|
+
}
|
|
3834
|
+
catch (error) {
|
|
3835
|
+
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
3836
|
+
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
3837
|
+
}
|
|
3838
|
+
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
3839
|
+
relation: table.relation,
|
|
3840
|
+
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
3841
|
+
}));
|
|
3842
|
+
let raw;
|
|
3843
|
+
try {
|
|
3844
|
+
raw = await provider.generate([
|
|
3845
|
+
{
|
|
3846
|
+
role: 'system',
|
|
3847
|
+
content: [
|
|
3848
|
+
'Repair one failed read-only analytical SQL statement.',
|
|
3849
|
+
'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
|
|
3850
|
+
'Preserve the requested grain, filters, output columns, and aggregation semantics.',
|
|
3851
|
+
input.sqlParams?.length
|
|
3852
|
+
? 'Preserve every positional parameter placeholder exactly; do not add, remove, or renumber placeholders.'
|
|
3853
|
+
: '',
|
|
3854
|
+
'Use only relations and columns present in the supplied schema context.',
|
|
3855
|
+
'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
|
|
3856
|
+
'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
|
|
3857
|
+
].join(' '),
|
|
3858
|
+
},
|
|
3859
|
+
{
|
|
3860
|
+
role: 'user',
|
|
3861
|
+
content: JSON.stringify({
|
|
3862
|
+
question: input.question,
|
|
3863
|
+
driver: input.targetConnection.driver,
|
|
3864
|
+
failure: firstFailure.redactedMessage,
|
|
3865
|
+
sql: input.sourceSql,
|
|
3866
|
+
parameters: input.sqlParams?.map((parameter) => ({
|
|
3867
|
+
name: parameter.name,
|
|
3868
|
+
placeholder: `$${parameter.position}`,
|
|
3869
|
+
})),
|
|
3870
|
+
schema: boundedSchema,
|
|
3871
|
+
}),
|
|
3872
|
+
},
|
|
3873
|
+
], { maxTokens: 1200, temperature: 0 });
|
|
3874
|
+
}
|
|
3875
|
+
catch {
|
|
3876
|
+
throw boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502);
|
|
3877
|
+
}
|
|
3878
|
+
const proposal = parseProposal(raw);
|
|
3879
|
+
if (!proposal.sql?.trim()) {
|
|
3880
|
+
throw boundedRepairError('The AI repair did not return an executable SQL statement.', 422);
|
|
3881
|
+
}
|
|
3882
|
+
repairedSql = proposal.sql.trim();
|
|
3883
|
+
const requiredPlaceholders = (input.sqlParams ?? []).map((parameter) => `$${parameter.position}`);
|
|
3884
|
+
if (requiredPlaceholders.some((placeholder) => !new RegExp(`${placeholder.replace('$', '\\$')}\\b`).test(repairedSql))) {
|
|
3885
|
+
throw boundedRepairError('The repair changed this cell\'s parameter contract, so DQL stopped it before execution.', 422);
|
|
3886
|
+
}
|
|
3887
|
+
repairMode = 'ai';
|
|
3888
|
+
try {
|
|
3889
|
+
result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables });
|
|
3890
|
+
}
|
|
3891
|
+
catch (error) {
|
|
3892
|
+
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
3893
|
+
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
// Keep preview-only row bounds out of editable notebook source. The one
|
|
3897
|
+
// deterministic source rewrite we do persist is decoding internal graph
|
|
3898
|
+
// identities, because those can never execute as warehouse SQL.
|
|
3899
|
+
let rewrites = [];
|
|
3900
|
+
if (internalRelationIdsInSql(repairedSql).length > 0) {
|
|
3901
|
+
const normalizedSource = await prepareAnalyticalExecutionSql({
|
|
3902
|
+
sql: repairedSql,
|
|
3903
|
+
subject: 'Repaired SQL',
|
|
3904
|
+
executor,
|
|
3905
|
+
connection: input.targetConnection,
|
|
3906
|
+
projectRoot,
|
|
3907
|
+
projectConfig,
|
|
3908
|
+
enforceReadOnly: true,
|
|
3909
|
+
});
|
|
3910
|
+
repairedSql = normalizedSource.decodedSql;
|
|
3911
|
+
rewrites = normalizedSource.rewrites;
|
|
3912
|
+
}
|
|
3913
|
+
return { result, repairedSql, repairMode, firstFailure, rewrites };
|
|
3914
|
+
};
|
|
3806
3915
|
const generateInvestigationSqlForApp = async (input) => {
|
|
3807
3916
|
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
3808
3917
|
const resolvedProvider = governed?.provider ?? null;
|
|
@@ -5593,15 +5702,79 @@ export async function startLocalServer(opts) {
|
|
|
5593
5702
|
}
|
|
5594
5703
|
if (req.method === 'GET' && path === '/api/modeling/dbt-first/inventory') {
|
|
5595
5704
|
const requestId = apiRequestId('modeling-inventory');
|
|
5596
|
-
const
|
|
5705
|
+
const physicalOnly = url.searchParams.get('physicalOnly') === 'true';
|
|
5706
|
+
const modelingEnabled = projectConfig.manifestVersion === 3 && projectConfig.modeling?.mode === 'dbt-first';
|
|
5707
|
+
if (physicalOnly && !modelingEnabled) {
|
|
5708
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5709
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
5710
|
+
requestId,
|
|
5711
|
+
code: 'DBT_FIRST_NOT_ENABLED',
|
|
5712
|
+
message: 'dbt-first modeling is not enabled. Connect and apply a dbt project from Settings → Project & dbt.',
|
|
5713
|
+
nextActions: ['Open Settings → Project & dbt and apply a valid dbt project.'],
|
|
5714
|
+
})));
|
|
5715
|
+
return;
|
|
5716
|
+
}
|
|
5717
|
+
const dbtProjectDir = findDbtProjectPath(projectRoot, projectConfig);
|
|
5718
|
+
const configuredManifestPath = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
|
|
5719
|
+
const expectedManifestPath = resolve(dbtProjectDir, configuredManifestPath);
|
|
5720
|
+
if (physicalOnly && !existsSync(expectedManifestPath)) {
|
|
5721
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5722
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
5723
|
+
requestId,
|
|
5724
|
+
code: 'DBT_MANIFEST_NOT_FOUND',
|
|
5725
|
+
message: `The configured dbt manifest was not found at ${configuredManifestPath}.`,
|
|
5726
|
+
details: { manifestPath: configuredManifestPath },
|
|
5727
|
+
nextActions: ['Run dbt parse, dbt compile, or dbt build, then refresh the database catalog.'],
|
|
5728
|
+
})));
|
|
5729
|
+
return;
|
|
5730
|
+
}
|
|
5731
|
+
let snapshot;
|
|
5732
|
+
if (physicalOnly) {
|
|
5733
|
+
try {
|
|
5734
|
+
snapshot = projectSnapshot();
|
|
5735
|
+
}
|
|
5736
|
+
catch (error) {
|
|
5737
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5738
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
5739
|
+
requestId,
|
|
5740
|
+
code: 'DBT_MANIFEST_COMPILE_FAILED',
|
|
5741
|
+
message: apiErrorMessage(error),
|
|
5742
|
+
details: { manifestPath: configuredManifestPath },
|
|
5743
|
+
nextActions: ['Fix or rebuild the dbt manifest, then refresh the database catalog.'],
|
|
5744
|
+
})));
|
|
5745
|
+
return;
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
else {
|
|
5749
|
+
snapshot = projectSnapshot();
|
|
5750
|
+
}
|
|
5597
5751
|
const manifest = snapshot.manifest;
|
|
5598
|
-
|
|
5752
|
+
if (physicalOnly && (manifest.manifestVersion !== 3 || !manifest.modeling || !manifest.dbtProvenance)) {
|
|
5753
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5754
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
5755
|
+
requestId,
|
|
5756
|
+
snapshotId: snapshot.snapshotId,
|
|
5757
|
+
code: 'DBT_MANIFEST_COMPILE_FAILED',
|
|
5758
|
+
message: snapshot.error ?? 'DQL could not compile the dbt database catalog.',
|
|
5759
|
+
details: { manifestPath: configuredManifestPath },
|
|
5760
|
+
nextActions: ['Review the dbt artifact, rebuild it, then refresh the database catalog.'],
|
|
5761
|
+
})));
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
// PERF-001 / CTX-005: only the Database browser opts into the smaller,
|
|
5765
|
+
// physical-relation contract. Existing modeling consumers retain their
|
|
5766
|
+
// original inventory scope and pagination defaults.
|
|
5767
|
+
const maxLimit = physicalOnly ? 100 : 200;
|
|
5768
|
+
const defaultLimit = physicalOnly ? 25 : 50;
|
|
5769
|
+
const limit = Math.min(maxLimit, Math.max(1, Number(url.searchParams.get('limit')) || defaultLimit));
|
|
5599
5770
|
const cursor = Math.max(0, Number(url.searchParams.get('cursor')) || 0);
|
|
5600
5771
|
const query = (url.searchParams.get('q') ?? '').trim().toLowerCase();
|
|
5601
5772
|
const queryTokens = query.split(/[^a-z0-9]+/).filter(Boolean);
|
|
5602
5773
|
const domain = (url.searchParams.get('domain') ?? '').trim();
|
|
5603
5774
|
const boundByDbtId = new Map(Object.values(manifest.modeling?.entities ?? {}).map((entity) => [entity.dbtUniqueId, entity]));
|
|
5775
|
+
const physicalIds = physicalOnly ? physicalDbtRelationIds(expectedManifestPath) : undefined;
|
|
5604
5776
|
const nodes = Object.values(manifest.dbtProvenance?.nodes ?? {})
|
|
5777
|
+
.filter((node) => !physicalOnly || (Boolean(node.relation) && (!physicalIds || physicalIds.has(node.uniqueId))))
|
|
5605
5778
|
.filter((node) => {
|
|
5606
5779
|
if (!queryTokens.length)
|
|
5607
5780
|
return true;
|
|
@@ -5634,6 +5807,11 @@ export async function startLocalServer(opts) {
|
|
|
5634
5807
|
nextCursor: cursor + limit < nodes.length ? cursor + limit : null,
|
|
5635
5808
|
total: nodes.length,
|
|
5636
5809
|
snapshotId: snapshot.snapshotId,
|
|
5810
|
+
...(physicalOnly ? {
|
|
5811
|
+
manifestPath: manifest.dbtProvenance.manifestPath,
|
|
5812
|
+
projectName: manifest.dbtProvenance.projectName,
|
|
5813
|
+
scope: 'dbt_relations',
|
|
5814
|
+
} : {}),
|
|
5637
5815
|
}));
|
|
5638
5816
|
return;
|
|
5639
5817
|
}
|
|
@@ -5870,97 +6048,25 @@ export async function startLocalServer(opts) {
|
|
|
5870
6048
|
: sourceRun.executionTarget?.target === 'local'
|
|
5871
6049
|
? 'local'
|
|
5872
6050
|
: projectConfig.defaultConnectionName;
|
|
5873
|
-
let
|
|
5874
|
-
let repairedSql = sourceSql;
|
|
5875
|
-
let repairMode = 'deterministic';
|
|
5876
|
-
let firstFailure;
|
|
6051
|
+
let repaired;
|
|
5877
6052
|
try {
|
|
5878
|
-
|
|
6053
|
+
repaired = await executeBoundedSqlRepair({
|
|
6054
|
+
question: sourceRun.question,
|
|
6055
|
+
sourceSql,
|
|
6056
|
+
targetConnection,
|
|
6057
|
+
targetConnectionName,
|
|
6058
|
+
});
|
|
5879
6059
|
}
|
|
5880
6060
|
catch (error) {
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
res.end(serializeJSON({
|
|
5887
|
-
error: firstFailure?.redactedMessage ?? 'The deterministic repair did not produce an executable query.',
|
|
5888
|
-
...(firstFailure ? { warehouseFailure: firstFailure } : {}),
|
|
5889
|
-
}));
|
|
5890
|
-
return;
|
|
5891
|
-
}
|
|
5892
|
-
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
5893
|
-
if (!provider) {
|
|
5894
|
-
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5895
|
-
res.end(serializeJSON({ error: 'Configure an available AI provider in Settings to repair this query.' }));
|
|
5896
|
-
return;
|
|
5897
|
-
}
|
|
5898
|
-
let schemaContext;
|
|
5899
|
-
try {
|
|
5900
|
-
schemaContext = await getSchemaContextForAgent(sourceRun.question, undefined, targetConnection, targetConnectionName);
|
|
5901
|
-
}
|
|
5902
|
-
catch (error) {
|
|
5903
|
-
const failure = normalizeWarehouseSqlFailure(error, targetConnection.driver);
|
|
5904
|
-
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5905
|
-
res.end(serializeJSON({ error: failure.redactedMessage, warehouseFailure: failure }));
|
|
5906
|
-
return;
|
|
5907
|
-
}
|
|
5908
|
-
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
5909
|
-
relation: table.relation,
|
|
5910
|
-
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
6061
|
+
const repairError = error;
|
|
6062
|
+
res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6063
|
+
res.end(serializeJSON({
|
|
6064
|
+
error: repairError.message,
|
|
6065
|
+
...(repairError.warehouseFailure ? { warehouseFailure: repairError.warehouseFailure } : {}),
|
|
5911
6066
|
}));
|
|
5912
|
-
|
|
5913
|
-
try {
|
|
5914
|
-
raw = await provider.generate([
|
|
5915
|
-
{
|
|
5916
|
-
role: 'system',
|
|
5917
|
-
content: [
|
|
5918
|
-
'Repair one failed read-only analytical SQL statement.',
|
|
5919
|
-
'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
|
|
5920
|
-
'Preserve the user question, selected grain, filters, and aggregation semantics.',
|
|
5921
|
-
'Use only relations and columns present in the supplied schema context.',
|
|
5922
|
-
'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
|
|
5923
|
-
'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
|
|
5924
|
-
].join(' '),
|
|
5925
|
-
},
|
|
5926
|
-
{
|
|
5927
|
-
role: 'user',
|
|
5928
|
-
content: JSON.stringify({
|
|
5929
|
-
question: sourceRun.question,
|
|
5930
|
-
driver: targetConnection.driver,
|
|
5931
|
-
failure: firstFailure.redactedMessage,
|
|
5932
|
-
sql: sourceSql,
|
|
5933
|
-
schema: boundedSchema,
|
|
5934
|
-
}),
|
|
5935
|
-
},
|
|
5936
|
-
], { maxTokens: 1200, temperature: 0 });
|
|
5937
|
-
}
|
|
5938
|
-
catch {
|
|
5939
|
-
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5940
|
-
res.end(serializeJSON({ error: 'The AI provider could not complete this repair. Check Provider Settings and retry.' }));
|
|
5941
|
-
return;
|
|
5942
|
-
}
|
|
5943
|
-
const proposal = parseProposal(raw);
|
|
5944
|
-
if (!proposal.sql?.trim()) {
|
|
5945
|
-
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5946
|
-
res.end(serializeJSON({ error: 'The AI repair did not return an executable SQL statement.' }));
|
|
5947
|
-
return;
|
|
5948
|
-
}
|
|
5949
|
-
repairedSql = proposal.sql.trim();
|
|
5950
|
-
repairMode = 'ai';
|
|
5951
|
-
try {
|
|
5952
|
-
result = await executeGeneratedSqlDirect(sourceRun.question, repairedSql, undefined, targetConnection);
|
|
5953
|
-
}
|
|
5954
|
-
catch (error) {
|
|
5955
|
-
const failure = normalizeWarehouseSqlFailure(error, targetConnection.driver);
|
|
5956
|
-
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5957
|
-
res.end(serializeJSON({
|
|
5958
|
-
error: failure.redactedMessage,
|
|
5959
|
-
warehouseFailure: failure,
|
|
5960
|
-
}));
|
|
5961
|
-
return;
|
|
5962
|
-
}
|
|
6067
|
+
return;
|
|
5963
6068
|
}
|
|
6069
|
+
const { result, repairedSql, repairMode, firstFailure } = repaired;
|
|
5964
6070
|
const completedAt = new Date().toISOString();
|
|
5965
6071
|
const preview = agentResultToSynthesisPreview(result);
|
|
5966
6072
|
const synthesis = await synthesizeAnswer({
|
|
@@ -10399,10 +10505,20 @@ export async function startLocalServer(opts) {
|
|
|
10399
10505
|
// of the same 7,500+ object catalog while keeping the route compatible
|
|
10400
10506
|
// for older clients.
|
|
10401
10507
|
const includeSemantic = url.searchParams.get('includeSemantic') !== 'false';
|
|
10508
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot, cfg);
|
|
10509
|
+
let databaseTree = [];
|
|
10510
|
+
if (cfg.manifestVersion === 3 && cfg.modeling?.mode === 'dbt-first' && dbtManifestPath) {
|
|
10511
|
+
try {
|
|
10512
|
+
databaseTree = buildDbtDatabaseSchemaTree(projectSnapshot().manifest, 25, physicalDbtRelationIds(dbtManifestPath));
|
|
10513
|
+
}
|
|
10514
|
+
catch {
|
|
10515
|
+
// Keep the rest of Block Studio available while dbt is rebuilt.
|
|
10516
|
+
}
|
|
10517
|
+
}
|
|
10402
10518
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10403
10519
|
res.end(serializeJSON({
|
|
10404
10520
|
semanticTree: includeSemantic && semanticLayer ? buildSemanticTree(semanticLayer, semanticImportManifest) : null,
|
|
10405
|
-
databaseTree
|
|
10521
|
+
databaseTree,
|
|
10406
10522
|
connection: {
|
|
10407
10523
|
default: defaultKey,
|
|
10408
10524
|
current: defaultKey,
|
|
@@ -12173,13 +12289,17 @@ export async function startLocalServer(opts) {
|
|
|
12173
12289
|
// GET /api/describe-table?table=schema.table — returns columns for a specific table
|
|
12174
12290
|
if (req.method === 'GET' && path === '/api/describe-table') {
|
|
12175
12291
|
try {
|
|
12176
|
-
const
|
|
12177
|
-
const
|
|
12178
|
-
|
|
12292
|
+
const requestedRelation = url.searchParams.get('relation')?.trim() ?? '';
|
|
12293
|
+
const legacyTable = url.searchParams.get('table')?.trim() ?? '';
|
|
12294
|
+
const legacySchema = url.searchParams.get('schema')?.trim() ?? '';
|
|
12295
|
+
const relationParts = splitQualifiedRelationIdentifier(requestedRelation || [legacySchema, legacyTable].filter(Boolean).join('.'));
|
|
12296
|
+
if (!relationParts?.length) {
|
|
12179
12297
|
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12180
|
-
res.end(serializeJSON({ error: '
|
|
12298
|
+
res.end(serializeJSON({ error: 'A safe dbt relation is required.' }));
|
|
12181
12299
|
return;
|
|
12182
12300
|
}
|
|
12301
|
+
const tablePath = relationParts.at(-1);
|
|
12302
|
+
const schemaName = relationParts.length > 1 ? relationParts.slice(0, -1).join('.') : undefined;
|
|
12183
12303
|
// Try connector.listColumns() first
|
|
12184
12304
|
let columns = [];
|
|
12185
12305
|
const activeConnection = connection;
|
|
@@ -12188,7 +12308,11 @@ export async function startLocalServer(opts) {
|
|
|
12188
12308
|
throw new Error('No active connection');
|
|
12189
12309
|
const connector = await executor.getConnector(activeConnection);
|
|
12190
12310
|
if (typeof connector.listColumns === 'function') {
|
|
12191
|
-
|
|
12311
|
+
// Connector contracts accept one schema plus one table. For a
|
|
12312
|
+
// three-part dbt relation, the active connection owns the database
|
|
12313
|
+
// and the final namespace segment is the schema.
|
|
12314
|
+
const connectorSchema = schemaName?.split('.').filter(Boolean).at(-1);
|
|
12315
|
+
const rawCols = await connector.listColumns(connectorSchema, tablePath);
|
|
12192
12316
|
columns = rawCols.map((c) => ({ name: c.name, type: c.dataType }));
|
|
12193
12317
|
}
|
|
12194
12318
|
}
|
|
@@ -12200,17 +12324,15 @@ export async function startLocalServer(opts) {
|
|
|
12200
12324
|
try {
|
|
12201
12325
|
const isFile = /\.(csv|parquet|json)$/i.test(tablePath) || tablePath.startsWith('data/');
|
|
12202
12326
|
const safePath = tablePath.replace(/'/g, "''");
|
|
12203
|
-
|
|
12327
|
+
if (!activeConnection)
|
|
12328
|
+
throw new Error('No active connection');
|
|
12329
|
+
const dialect = getDialect(activeConnection.driver);
|
|
12330
|
+
const qualifiedIdentifier = relationParts.map((part) => dialect.quoteIdentifier(part)).join('.');
|
|
12204
12331
|
const sql = isFile
|
|
12205
12332
|
? `DESCRIBE SELECT * FROM read_csv_auto('${safePath}') LIMIT 0`
|
|
12206
12333
|
: `DESCRIBE ${qualifiedIdentifier}`;
|
|
12207
|
-
if (!activeConnection)
|
|
12208
|
-
throw new Error('No active connection');
|
|
12209
12334
|
const result = await executor.executeQuery(sql, [], {}, activeConnection);
|
|
12210
|
-
columns = result.rows
|
|
12211
|
-
name: String(row['column_name'] ?? row['Field'] ?? ''),
|
|
12212
|
-
type: String(row['column_type'] ?? row['Type'] ?? ''),
|
|
12213
|
-
}));
|
|
12335
|
+
columns = schemaColumnsFromDescribeRows(result.rows);
|
|
12214
12336
|
}
|
|
12215
12337
|
catch {
|
|
12216
12338
|
// empty columns
|
|
@@ -12299,6 +12421,187 @@ export async function startLocalServer(opts) {
|
|
|
12299
12421
|
}
|
|
12300
12422
|
return;
|
|
12301
12423
|
}
|
|
12424
|
+
/**
|
|
12425
|
+
* Repair an editable notebook cell without starting Notebook AI research.
|
|
12426
|
+
* The returned source is applied only to the in-memory notebook cell by the
|
|
12427
|
+
* client; this endpoint never writes a block/domain/dbt source file.
|
|
12428
|
+
*/
|
|
12429
|
+
if (req.method === 'POST' && path === '/api/notebook/repair-execution') {
|
|
12430
|
+
let body = {};
|
|
12431
|
+
let execContext = null;
|
|
12432
|
+
const start = Date.now();
|
|
12433
|
+
try {
|
|
12434
|
+
body = await readJSON(req);
|
|
12435
|
+
execContext = notebookExecutionContext(body.executionContext);
|
|
12436
|
+
const cell = normalizeNotebookCell(body.cell);
|
|
12437
|
+
if (!cell || (cell.type !== 'sql' && cell.type !== 'dql')) {
|
|
12438
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12439
|
+
res.end(serializeJSON({ error: 'An editable SQL or DQL notebook cell is required.' }));
|
|
12440
|
+
return;
|
|
12441
|
+
}
|
|
12442
|
+
if (cell.type === 'dql' && /^\s*@block\(/i.test(cell.source)) {
|
|
12443
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12444
|
+
res.end(serializeJSON({ error: 'Customize the bound block as a notebook copy before repairing it.' }));
|
|
12445
|
+
return;
|
|
12446
|
+
}
|
|
12447
|
+
const embeddedSourceSql = notebookCellRepairSql(cell);
|
|
12448
|
+
if (!embeddedSourceSql) {
|
|
12449
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12450
|
+
res.end(serializeJSON({ error: 'Background repair supports SQL cells and DQL cells with an embedded query. Edit this semantic DQL definition directly.' }));
|
|
12451
|
+
return;
|
|
12452
|
+
}
|
|
12453
|
+
if (cell.type === 'sql' && /\{\{[^}]+\}\}/.test(cell.source)) {
|
|
12454
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12455
|
+
res.end(serializeJSON({ error: 'Run or repair the referenced upstream cell before repairing this dependent query.' }));
|
|
12456
|
+
return;
|
|
12457
|
+
}
|
|
12458
|
+
const targetConnection = await resolveExecutionConnection(body);
|
|
12459
|
+
const submittedError = typeof body.error === 'string' ? body.error.trim() : '';
|
|
12460
|
+
const submittedFailure = body.failure && typeof body.failure === 'object' && !Array.isArray(body.failure)
|
|
12461
|
+
? body.failure
|
|
12462
|
+
: undefined;
|
|
12463
|
+
const failureCode = typeof submittedFailure?.code === 'string' ? submittedFailure.code : undefined;
|
|
12464
|
+
if (!notebookFailureAllowsBackgroundRepair({ code: failureCode, message: submittedError })) {
|
|
12465
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12466
|
+
res.end(serializeJSON({ error: 'This failure needs a data, access, parameter, or notebook dependency change and cannot be auto-fixed safely.' }));
|
|
12467
|
+
return;
|
|
12468
|
+
}
|
|
12469
|
+
const retainedFailure = submittedError
|
|
12470
|
+
? normalizeWarehouseSqlFailure({ code: failureCode, message: submittedError }, targetConnection.driver)
|
|
12471
|
+
: undefined;
|
|
12472
|
+
if (!analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
|
|
12473
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12474
|
+
res.end(serializeJSON({
|
|
12475
|
+
error: 'This failure requires authorized access or a policy change and cannot be repaired in the background.',
|
|
12476
|
+
warehouseFailure: retainedFailure,
|
|
12477
|
+
}));
|
|
12478
|
+
return;
|
|
12479
|
+
}
|
|
12480
|
+
const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
12481
|
+
? body.parameters
|
|
12482
|
+
: {};
|
|
12483
|
+
const invocation = cell.type === 'dql'
|
|
12484
|
+
? prepareBlockInvocation({ source: cell.source, parameters, surface: 'notebook' })
|
|
12485
|
+
: null;
|
|
12486
|
+
if (invocation?.errors.length) {
|
|
12487
|
+
throw boundedRepairError(invocation.errors.join(' '), 409);
|
|
12488
|
+
}
|
|
12489
|
+
if (invocation?.unresolvedParameters.length) {
|
|
12490
|
+
throw boundedRepairError(`Provide required parameter${invocation.unresolvedParameters.length === 1 ? '' : 's'} before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
|
|
12491
|
+
}
|
|
12492
|
+
const plan = cell.type === 'dql'
|
|
12493
|
+
? buildExecutionPlan(cell, {
|
|
12494
|
+
semanticLayer,
|
|
12495
|
+
driver: targetConnection.driver,
|
|
12496
|
+
parameters: invocation?.values,
|
|
12497
|
+
})
|
|
12498
|
+
: null;
|
|
12499
|
+
const attemptedSql = typeof body.attemptedSql === 'string' && body.attemptedSql.trim()
|
|
12500
|
+
? body.attemptedSql.trim()
|
|
12501
|
+
: embeddedSourceSql;
|
|
12502
|
+
const sourceSql = plan?.sql ?? attemptedSql;
|
|
12503
|
+
const sqlParams = plan?.sqlParams ?? [];
|
|
12504
|
+
const variables = {
|
|
12505
|
+
...(plan?.variables ?? {}),
|
|
12506
|
+
...(invocation?.values ?? {}),
|
|
12507
|
+
};
|
|
12508
|
+
const executionTarget = executionTargetDescriptor(body);
|
|
12509
|
+
const targetConnectionName = executionTarget.target === 'connection'
|
|
12510
|
+
? executionTarget.connectionName
|
|
12511
|
+
: executionTarget.target === 'local'
|
|
12512
|
+
? 'local'
|
|
12513
|
+
: projectConfig.defaultConnectionName;
|
|
12514
|
+
const question = [
|
|
12515
|
+
`Repair notebook cell ${cell.title?.trim() || cell.id}.`,
|
|
12516
|
+
submittedError ? `Failure: ${submittedError.slice(0, 1_000)}` : '',
|
|
12517
|
+
].filter(Boolean).join(' ');
|
|
12518
|
+
const repaired = await executeBoundedSqlRepair({
|
|
12519
|
+
question,
|
|
12520
|
+
sourceSql,
|
|
12521
|
+
targetConnection,
|
|
12522
|
+
targetConnectionName,
|
|
12523
|
+
sqlParams,
|
|
12524
|
+
variables,
|
|
12525
|
+
});
|
|
12526
|
+
let repairedSource;
|
|
12527
|
+
if (cell.type === 'dql') {
|
|
12528
|
+
const originalQuery = notebookCellRepairSql(cell);
|
|
12529
|
+
const repairedQuery = repaired.repairMode === 'deterministic'
|
|
12530
|
+
? applyNotebookRepairRewrites(originalQuery, repaired.rewrites)
|
|
12531
|
+
: restoreNotebookDqlParameterInterpolations(repaired.repairedSql, sqlParams);
|
|
12532
|
+
repairedSource = replaceNotebookDqlQueryForRepair(cell.source, repairedQuery);
|
|
12533
|
+
}
|
|
12534
|
+
else {
|
|
12535
|
+
repairedSource = repaired.repairedSql;
|
|
12536
|
+
}
|
|
12537
|
+
if (!repairedSource) {
|
|
12538
|
+
throw boundedRepairError('DQL could not safely replace the embedded query in this cell.', 422);
|
|
12539
|
+
}
|
|
12540
|
+
// Prove the repaired DQL wrapper still compiles. SQL execution above
|
|
12541
|
+
// already proved the corrected statement on the selected target.
|
|
12542
|
+
if (cell.type === 'dql') {
|
|
12543
|
+
const validationPlan = buildExecutionPlan({ ...cell, source: repairedSource }, {
|
|
12544
|
+
semanticLayer,
|
|
12545
|
+
driver: targetConnection.driver,
|
|
12546
|
+
parameters: invocation?.values,
|
|
12547
|
+
});
|
|
12548
|
+
if (!validationPlan?.sql) {
|
|
12549
|
+
throw boundedRepairError('The repaired SQL ran, but the DQL wrapper no longer compiles.', 422);
|
|
12550
|
+
}
|
|
12551
|
+
const beforeParameters = sqlParams.map((parameter) => `${parameter.position}:${parameter.name}`);
|
|
12552
|
+
const afterParameters = validationPlan.sqlParams.map((parameter) => `${parameter.position}:${parameter.name}`);
|
|
12553
|
+
if (beforeParameters.join('|') !== afterParameters.join('|')) {
|
|
12554
|
+
throw boundedRepairError('The repaired DQL changed the cell parameter contract, so the update was stopped.', 422);
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12557
|
+
if (execContext?.notebookPath) {
|
|
12558
|
+
recordNotebookQueryRun(projectRoot, {
|
|
12559
|
+
notebookPath: execContext.notebookPath,
|
|
12560
|
+
cellId: execContext.cellId ?? cell.id,
|
|
12561
|
+
cellName: execContext.cellName ?? cell.title,
|
|
12562
|
+
source: execContext.source ?? (cell.type === 'dql' ? 'notebook_dql_cell' : 'notebook_sql_cell'),
|
|
12563
|
+
status: 'success',
|
|
12564
|
+
rowCount: repaired.result.rowCount ?? repaired.result.rows.length,
|
|
12565
|
+
durationMs: Date.now() - start,
|
|
12566
|
+
sql: repaired.repairedSql,
|
|
12567
|
+
});
|
|
12568
|
+
}
|
|
12569
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12570
|
+
res.end(serializeJSON({
|
|
12571
|
+
ok: true,
|
|
12572
|
+
source: repairedSource,
|
|
12573
|
+
repairedSql: repaired.repairedSql,
|
|
12574
|
+
repairMode: repaired.repairMode,
|
|
12575
|
+
result: repaired.result,
|
|
12576
|
+
executionTarget,
|
|
12577
|
+
executionContext: notebookExecutionIdentity(execContext, cell.id),
|
|
12578
|
+
compiledSql: repaired.repairedSql,
|
|
12579
|
+
executedSql: repaired.result.sql ?? repaired.repairedSql,
|
|
12580
|
+
executionReceipt: repaired.result.executionReceipt,
|
|
12581
|
+
}));
|
|
12582
|
+
}
|
|
12583
|
+
catch (error) {
|
|
12584
|
+
const repairError = error;
|
|
12585
|
+
if (execContext?.notebookPath) {
|
|
12586
|
+
recordNotebookQueryRun(projectRoot, {
|
|
12587
|
+
notebookPath: execContext.notebookPath,
|
|
12588
|
+
cellId: execContext.cellId,
|
|
12589
|
+
cellName: execContext.cellName,
|
|
12590
|
+
source: execContext.source ?? 'notebook_cell_repair',
|
|
12591
|
+
status: 'error',
|
|
12592
|
+
durationMs: Date.now() - start,
|
|
12593
|
+
errorCode: repairError.warehouseFailure?.category ?? repairError.message,
|
|
12594
|
+
sql: typeof body.attemptedSql === 'string' ? body.attemptedSql : undefined,
|
|
12595
|
+
});
|
|
12596
|
+
}
|
|
12597
|
+
res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12598
|
+
res.end(serializeJSON({
|
|
12599
|
+
error: repairError.message,
|
|
12600
|
+
...(repairError.warehouseFailure ? { warehouseFailure: repairError.warehouseFailure } : {}),
|
|
12601
|
+
}));
|
|
12602
|
+
}
|
|
12603
|
+
return;
|
|
12604
|
+
}
|
|
12302
12605
|
if (req.method === 'POST' && path === '/api/query') {
|
|
12303
12606
|
let body;
|
|
12304
12607
|
let execContext = null;
|
|
@@ -16846,6 +17149,55 @@ function normalizeNotebookCell(value) {
|
|
|
16846
17149
|
config: candidate.config,
|
|
16847
17150
|
};
|
|
16848
17151
|
}
|
|
17152
|
+
export function notebookCellRepairSql(cell) {
|
|
17153
|
+
if (cell.type === 'sql')
|
|
17154
|
+
return cell.source.trim() || undefined;
|
|
17155
|
+
if (cell.type !== 'dql')
|
|
17156
|
+
return undefined;
|
|
17157
|
+
const match = /\bquery\s*=\s*"""([\s\S]*?)"""/i.exec(cell.source);
|
|
17158
|
+
return match?.[1]?.trim() || undefined;
|
|
17159
|
+
}
|
|
17160
|
+
export function replaceNotebookDqlQueryForRepair(source, sql) {
|
|
17161
|
+
const queryField = /\b(query\s*=\s*""")([\s\S]*?)(""")/i;
|
|
17162
|
+
if (!queryField.test(source) || !sql.trim())
|
|
17163
|
+
return undefined;
|
|
17164
|
+
return source.replace(queryField, (_match, open, _body, close) => (`${open}\n${sql.trim()}\n ${close}`));
|
|
17165
|
+
}
|
|
17166
|
+
const NOTEBOOK_BACKGROUND_REPAIR_BLOCKED_CODES = new Set([
|
|
17167
|
+
'AMBIGUOUS_NOTEBOOK_DEPENDENCY',
|
|
17168
|
+
'CROSS_ENGINE_JOIN_REQUIRED',
|
|
17169
|
+
'EXECUTION_TARGET_MISMATCH',
|
|
17170
|
+
'SEMANTIC_SOURCE_DRIFT',
|
|
17171
|
+
'STALE_CELL_EXECUTION',
|
|
17172
|
+
'UPSTREAM_RESULT_UNAVAILABLE',
|
|
17173
|
+
'UNAUTHORIZED',
|
|
17174
|
+
]);
|
|
17175
|
+
/**
|
|
17176
|
+
* Notebook background repair is deliberately narrower than conversational
|
|
17177
|
+
* repair. Access, policy, target, parameter, and dependency failures require a
|
|
17178
|
+
* user decision; only query-shape failures may enter the one-attempt loop.
|
|
17179
|
+
* Acceptance: API-006, API-007, UI-012, UI-013, SEC-004, E2E-014.
|
|
17180
|
+
*/
|
|
17181
|
+
export function notebookFailureAllowsBackgroundRepair(input) {
|
|
17182
|
+
const code = input.code?.trim().toUpperCase();
|
|
17183
|
+
const message = input.message?.trim() ?? '';
|
|
17184
|
+
if (!message)
|
|
17185
|
+
return false;
|
|
17186
|
+
if (code && NOTEBOOK_BACKGROUND_REPAIR_BLOCKED_CODES.has(code))
|
|
17187
|
+
return false;
|
|
17188
|
+
return !/(?:permission|not authorized|unauthori[sz]ed|access denied|authentication|credential|policy|unsafe|read[- ]only|cancel(?:led|ed)|required parameters?|provide required|cross[- ]engine|upstream (?:cell|result)|notebook dependency|target mismatch|source drift|matched more than one schema)/i.test(message);
|
|
17189
|
+
}
|
|
17190
|
+
export function applyNotebookRepairRewrites(sql, rewrites) {
|
|
17191
|
+
return rewrites.reduce((current, rewrite) => {
|
|
17192
|
+
const pattern = rewrite.from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17193
|
+
return current.replace(new RegExp(pattern, 'g'), rewrite.to);
|
|
17194
|
+
}, sql);
|
|
17195
|
+
}
|
|
17196
|
+
export function restoreNotebookDqlParameterInterpolations(sql, parameters) {
|
|
17197
|
+
return [...parameters]
|
|
17198
|
+
.sort((left, right) => right.position - left.position)
|
|
17199
|
+
.reduce((current, parameter) => current.replace(new RegExp(`\\$${parameter.position}\\b`, 'g'), `\${${parameter.name}}`), sql);
|
|
17200
|
+
}
|
|
16849
17201
|
function resolveNotebookBlockReferenceCell(cell, projectRoot) {
|
|
16850
17202
|
if (cell.type !== 'dql')
|
|
16851
17203
|
return { cell };
|
|
@@ -17039,6 +17391,68 @@ function readUserPrefs(userPrefsPath) {
|
|
|
17039
17391
|
function writeUserPrefs(userPrefsPath, prefs) {
|
|
17040
17392
|
writeFileSync(userPrefsPath, JSON.stringify(prefs, null, 2) + '\n', 'utf-8');
|
|
17041
17393
|
}
|
|
17394
|
+
/** Normalize DESCRIBE output across Snowflake, DuckDB, and PostgreSQL shapes. */
|
|
17395
|
+
export function schemaColumnsFromDescribeRows(rows) {
|
|
17396
|
+
return rows.map((row) => ({
|
|
17397
|
+
name: String(row['column_name'] ?? row['COLUMN_NAME'] ?? row['Field'] ?? row['FIELD'] ?? row['name'] ?? row['NAME'] ?? ''),
|
|
17398
|
+
type: String(row['column_type'] ?? row['COLUMN_TYPE'] ?? row['data_type'] ?? row['DATA_TYPE'] ?? row['Type'] ?? row['TYPE'] ?? row['type'] ?? ''),
|
|
17399
|
+
})).filter((column) => Boolean(column.name));
|
|
17400
|
+
}
|
|
17401
|
+
/** Parse dbt relation strings without letting relation text become SQL syntax. */
|
|
17402
|
+
export function splitQualifiedRelationIdentifier(value) {
|
|
17403
|
+
const rawParts = [];
|
|
17404
|
+
let current = '';
|
|
17405
|
+
let quote = null;
|
|
17406
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
17407
|
+
const char = value[index];
|
|
17408
|
+
if (quote) {
|
|
17409
|
+
current += char;
|
|
17410
|
+
if (char === quote) {
|
|
17411
|
+
const next = value[index + 1];
|
|
17412
|
+
if ((quote === '"' || quote === '`') && next === quote) {
|
|
17413
|
+
current += next;
|
|
17414
|
+
index += 1;
|
|
17415
|
+
}
|
|
17416
|
+
else {
|
|
17417
|
+
quote = null;
|
|
17418
|
+
}
|
|
17419
|
+
}
|
|
17420
|
+
continue;
|
|
17421
|
+
}
|
|
17422
|
+
if (char === '"' || char === '`') {
|
|
17423
|
+
quote = char;
|
|
17424
|
+
current += char;
|
|
17425
|
+
continue;
|
|
17426
|
+
}
|
|
17427
|
+
if (char === '[') {
|
|
17428
|
+
quote = ']';
|
|
17429
|
+
current += char;
|
|
17430
|
+
continue;
|
|
17431
|
+
}
|
|
17432
|
+
if (char === '.') {
|
|
17433
|
+
rawParts.push(current);
|
|
17434
|
+
current = '';
|
|
17435
|
+
continue;
|
|
17436
|
+
}
|
|
17437
|
+
current += char;
|
|
17438
|
+
}
|
|
17439
|
+
if (quote)
|
|
17440
|
+
return null;
|
|
17441
|
+
rawParts.push(current);
|
|
17442
|
+
if (rawParts.length === 0 || rawParts.length > 4)
|
|
17443
|
+
return null;
|
|
17444
|
+
const decoded = rawParts.map((raw) => {
|
|
17445
|
+
const token = raw.trim();
|
|
17446
|
+
if (/^"(?:[^"]|"")+"$/.test(token))
|
|
17447
|
+
return token.slice(1, -1).replace(/""/g, '"');
|
|
17448
|
+
if (/^`(?:[^`]|``)+`$/.test(token))
|
|
17449
|
+
return token.slice(1, -1).replace(/``/g, '`');
|
|
17450
|
+
if (/^\[[^\]]+\]$/.test(token))
|
|
17451
|
+
return token.slice(1, -1).replace(/\]\]/g, ']');
|
|
17452
|
+
return /^[A-Za-z_][A-Za-z0-9_$]*$/.test(token) ? token : null;
|
|
17453
|
+
});
|
|
17454
|
+
return decoded.every((part) => Boolean(part)) ? decoded : null;
|
|
17455
|
+
}
|
|
17042
17456
|
async function introspectSchema(executor, connection, options = {}) {
|
|
17043
17457
|
let tables = [];
|
|
17044
17458
|
const columnsByPath = new Map();
|
|
@@ -17093,77 +17507,93 @@ async function introspectSchema(executor, connection, options = {}) {
|
|
|
17093
17507
|
}
|
|
17094
17508
|
return { tables, columnsByPath };
|
|
17095
17509
|
}
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
const
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
// Eagerly resolve file columns via DuckDB DESCRIBE
|
|
17128
|
-
if (dataFiles.length > 0) {
|
|
17129
|
-
const fileChildren = [];
|
|
17130
|
-
for (const file of dataFiles) {
|
|
17131
|
-
let columns = [];
|
|
17132
|
-
if (connection) {
|
|
17133
|
-
try {
|
|
17134
|
-
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
17135
|
-
const readFn = ext === 'parquet' ? 'read_parquet' : ext === 'json' ? 'read_json_auto' : 'read_csv_auto';
|
|
17136
|
-
const descResult = await executor.executeQuery(`DESCRIBE SELECT * FROM ${readFn}('${file.path.replace(/'/g, "''")}') LIMIT 0`, [], {}, connection);
|
|
17137
|
-
columns = descResult.rows.map((row) => ({
|
|
17138
|
-
id: `db-column:${file.path}:${String(row['column_name'] ?? '')}`,
|
|
17139
|
-
label: String(row['column_name'] ?? ''),
|
|
17140
|
-
kind: 'column',
|
|
17141
|
-
path: file.path,
|
|
17142
|
-
type: String(row['column_type'] ?? ''),
|
|
17143
|
-
}));
|
|
17144
|
-
}
|
|
17145
|
-
catch {
|
|
17146
|
-
// file column discovery failed — empty children is fine
|
|
17147
|
-
}
|
|
17148
|
-
}
|
|
17149
|
-
fileChildren.push({
|
|
17150
|
-
id: `db-table:${file.path}`,
|
|
17151
|
-
label: file.name,
|
|
17152
|
-
kind: 'table',
|
|
17153
|
-
path: file.path,
|
|
17154
|
-
type: 'FILE',
|
|
17155
|
-
children: columns,
|
|
17156
|
-
});
|
|
17510
|
+
const dbtPhysicalRelationCache = new Map();
|
|
17511
|
+
/**
|
|
17512
|
+
* Display-only physical relation filter sourced from the raw dbt artifact.
|
|
17513
|
+
* This deliberately does not mutate DQLManifest or the agent metadata/indexes.
|
|
17514
|
+
*/
|
|
17515
|
+
function physicalDbtRelationIds(manifestPath) {
|
|
17516
|
+
try {
|
|
17517
|
+
const stat = statSync(manifestPath);
|
|
17518
|
+
const version = `${stat.size}:${stat.mtimeMs}`;
|
|
17519
|
+
const cached = dbtPhysicalRelationCache.get(manifestPath);
|
|
17520
|
+
if (cached?.version === version)
|
|
17521
|
+
return cached.ids;
|
|
17522
|
+
const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
17523
|
+
const ids = new Set();
|
|
17524
|
+
for (const collectionName of ['nodes', 'sources']) {
|
|
17525
|
+
const collection = raw[collectionName];
|
|
17526
|
+
if (!collection || typeof collection !== 'object' || Array.isArray(collection))
|
|
17527
|
+
continue;
|
|
17528
|
+
for (const [fallbackId, value] of Object.entries(collection)) {
|
|
17529
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17530
|
+
continue;
|
|
17531
|
+
const node = value;
|
|
17532
|
+
const resourceType = String(node.resource_type ?? (collectionName === 'sources' ? 'source' : ''));
|
|
17533
|
+
if (resourceType !== 'model' && resourceType !== 'source')
|
|
17534
|
+
continue;
|
|
17535
|
+
const config = node.config && typeof node.config === 'object' && !Array.isArray(node.config)
|
|
17536
|
+
? node.config
|
|
17537
|
+
: {};
|
|
17538
|
+
if (resourceType === 'model' && String(config.materialized ?? '').toLowerCase() === 'ephemeral')
|
|
17539
|
+
continue;
|
|
17540
|
+
ids.add(typeof node.unique_id === 'string' ? node.unique_id : fallbackId);
|
|
17157
17541
|
}
|
|
17158
|
-
databaseNodes.unshift({
|
|
17159
|
-
id: 'db-schema:files',
|
|
17160
|
-
label: 'files',
|
|
17161
|
-
kind: 'schema',
|
|
17162
|
-
children: fileChildren,
|
|
17163
|
-
});
|
|
17164
17542
|
}
|
|
17165
|
-
|
|
17166
|
-
|
|
17543
|
+
dbtPhysicalRelationCache.set(manifestPath, { version, ids });
|
|
17544
|
+
return ids;
|
|
17545
|
+
}
|
|
17546
|
+
catch {
|
|
17547
|
+
return undefined;
|
|
17548
|
+
}
|
|
17549
|
+
}
|
|
17550
|
+
export function buildDbtDatabaseSchemaTree(manifest, limit = 25, physicalIds) {
|
|
17551
|
+
// PERF-001 / CTX-005: Block Studio must not enumerate the connected account.
|
|
17552
|
+
// Its initial editor context is the first bounded page of physical relations
|
|
17553
|
+
// declared by the connected dbt artifact; columns remain lazy point-lookups.
|
|
17554
|
+
const boundedLimit = Math.min(100, Math.max(1, Math.floor(limit)));
|
|
17555
|
+
const seen = new Set();
|
|
17556
|
+
const tables = Object.values(manifest.dbtProvenance?.nodes ?? {})
|
|
17557
|
+
.filter((node) => Boolean(node.relation) && (!physicalIds || physicalIds.has(node.uniqueId)))
|
|
17558
|
+
.sort((a, b) => a.uniqueId.localeCompare(b.uniqueId))
|
|
17559
|
+
.flatMap((node) => {
|
|
17560
|
+
const path = node.relation;
|
|
17561
|
+
const key = path.toLowerCase();
|
|
17562
|
+
if (seen.has(key))
|
|
17563
|
+
return [];
|
|
17564
|
+
seen.add(key);
|
|
17565
|
+
const parts = path.split('.').filter(Boolean);
|
|
17566
|
+
return [{
|
|
17567
|
+
schema: parts.length > 1 ? parts.slice(0, -1).join('.') : 'default',
|
|
17568
|
+
name: parts.at(-1) ?? node.name,
|
|
17569
|
+
path,
|
|
17570
|
+
type: node.resourceType === 'source' ? 'DBT_SOURCE' : 'DBT_MODEL',
|
|
17571
|
+
}];
|
|
17572
|
+
})
|
|
17573
|
+
.slice(0, boundedLimit);
|
|
17574
|
+
const bySchema = new Map();
|
|
17575
|
+
for (const table of tables) {
|
|
17576
|
+
const current = bySchema.get(table.schema) ?? [];
|
|
17577
|
+
current.push(table);
|
|
17578
|
+
bySchema.set(table.schema, current);
|
|
17579
|
+
}
|
|
17580
|
+
return Array.from(bySchema.entries())
|
|
17581
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
17582
|
+
.map(([schema, schemaTables]) => ({
|
|
17583
|
+
id: `db-schema:${schema}`,
|
|
17584
|
+
label: schema,
|
|
17585
|
+
kind: 'schema',
|
|
17586
|
+
children: schemaTables
|
|
17587
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
17588
|
+
.map((table) => ({
|
|
17589
|
+
id: `db-table:${table.path}`,
|
|
17590
|
+
label: table.name,
|
|
17591
|
+
kind: 'table',
|
|
17592
|
+
path: table.path,
|
|
17593
|
+
type: table.type,
|
|
17594
|
+
children: [],
|
|
17595
|
+
})),
|
|
17596
|
+
}));
|
|
17167
17597
|
}
|
|
17168
17598
|
export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer) {
|
|
17169
17599
|
const normalizedPath = normalize(relativePath).replace(/^\/+/, '');
|