@duckcodeailabs/dql-cli 1.6.0 → 1.6.2
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/README.md +9 -9
- package/dist/apps-api.d.ts +20 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +82 -4
- package/dist/apps-api.js.map +1 -1
- package/dist/apps-api.test.js +43 -1
- package/dist/apps-api.test.js.map +1 -1
- package/dist/args.d.ts +2 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +3 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/index-60sOoPrg.js +3599 -0
- package/dist/assets/dql-notebook/assets/index-RaDW1A5g.css +1 -0
- package/dist/assets/dql-notebook/index.html +3 -3
- package/dist/commands/app.d.ts +4 -3
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +161 -75
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +7 -1
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/certify.d.ts.map +1 -1
- package/dist/commands/certify.js +35 -5
- package/dist/commands/certify.js.map +1 -1
- package/dist/commands/compile.d.ts +1 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +42 -3
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +91 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +1 -0
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/info.js +21 -6
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +4 -0
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/lineage.d.ts +3 -0
- package/dist/commands/lineage.d.ts.map +1 -1
- package/dist/commands/lineage.js +230 -2
- package/dist/commands/lineage.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +2 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/new.js +61 -2
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/new.test.js +106 -0
- package/dist/commands/new.test.js.map +1 -1
- package/dist/commands/parse.d.ts.map +1 -1
- package/dist/commands/parse.js +13 -3
- package/dist/commands/parse.js.map +1 -1
- package/dist/commands/preview.d.ts.map +1 -1
- package/dist/commands/preview.js +7 -1
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +55 -8
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/validate.test.js +85 -0
- package/dist/commands/validate.test.js.map +1 -1
- package/dist/commands/verify.d.ts +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +40 -6
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +136 -64
- package/dist/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +95 -19
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/tools.d.ts.map +1 -1
- package/dist/llm/tools.js +29 -1
- package/dist/llm/tools.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 +14 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +763 -58
- package/dist/local-runtime.js.map +1 -1
- package/dist/local-runtime.test.js +64 -1
- package/dist/local-runtime.test.js.map +1 -1
- package/dist/template-adoption.test.d.ts +2 -0
- package/dist/template-adoption.test.d.ts.map +1 -0
- package/dist/template-adoption.test.js +105 -0
- package/dist/template-adoption.test.js.map +1 -0
- package/package.json +13 -13
- package/dist/assets/dql-notebook/assets/index-B5jI3I8Q.js +0 -869
- package/dist/assets/dql-notebook/assets/index-cv-O4BEj.css +0 -1
- package/dist/package.json +0 -44
package/dist/local-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
5
6
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
6
7
|
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, } from '@duckcodeailabs/dql-core';
|
|
@@ -120,7 +121,8 @@ export async function startLocalServer(opts) {
|
|
|
120
121
|
config: (sourceCell.chartConfig ?? sourceCell.config),
|
|
121
122
|
};
|
|
122
123
|
const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
|
|
123
|
-
const
|
|
124
|
+
const tableMapping = await resolveSemanticTableMapping(executor, connection, semanticLayer);
|
|
125
|
+
const plan = buildExecutionPlan(resolved.cell, { semanticLayer, driver: connection.driver, tableMapping });
|
|
124
126
|
if (!plan) {
|
|
125
127
|
snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
|
|
126
128
|
continue;
|
|
@@ -181,15 +183,17 @@ export async function startLocalServer(opts) {
|
|
|
181
183
|
}
|
|
182
184
|
const absBlockPath = join(projectRoot, block.filePath);
|
|
183
185
|
const source = readFileSync(absBlockPath, 'utf-8');
|
|
186
|
+
const tableMapping = await resolveSemanticTableMapping(executor, connection, semanticLayer);
|
|
184
187
|
const semanticCompose = semanticLayer
|
|
185
188
|
? composeSemanticBlockSql(source, semanticLayer, {
|
|
186
189
|
driver: connection.driver,
|
|
190
|
+
tableMapping,
|
|
187
191
|
projectRoot,
|
|
188
192
|
projectConfig,
|
|
189
193
|
detectedProvider: semanticDetectedProvider,
|
|
190
194
|
})
|
|
191
195
|
: null;
|
|
192
|
-
const plan = buildExecutionPlan({ id: `agent-${block.name}`, type: 'dql', source, title: block.name }, { semanticLayer, driver: connection.driver });
|
|
196
|
+
const plan = buildExecutionPlan({ id: `agent-${block.name}`, type: 'dql', source, title: block.name }, { semanticLayer, driver: connection.driver, tableMapping });
|
|
193
197
|
if (!plan && !semanticCompose?.sql) {
|
|
194
198
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
195
199
|
throw new Error(semanticError ?? `Block "${block.name}" produced no executable SQL.`);
|
|
@@ -210,6 +214,37 @@ export async function startLocalServer(opts) {
|
|
|
210
214
|
blockPath: block.filePath,
|
|
211
215
|
};
|
|
212
216
|
};
|
|
217
|
+
const executeGeneratedSqlForAgent = async (sql) => {
|
|
218
|
+
const boundedSql = buildAgentPreviewSql(sql);
|
|
219
|
+
const semantic = prepareSemanticSql(boundedSql, semanticLayer);
|
|
220
|
+
if (semantic.unresolvedRefs.length > 0) {
|
|
221
|
+
throw new Error(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
const prepared = prepareLocalExecution(semantic.sql, connection, projectRoot, projectConfig);
|
|
224
|
+
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
225
|
+
assertAppAccess({ app, domain: app?.domain, level: 'execute' });
|
|
226
|
+
const rawResult = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
|
|
227
|
+
const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
|
|
228
|
+
return {
|
|
229
|
+
columns: normalized.columns,
|
|
230
|
+
rows: normalized.rows,
|
|
231
|
+
rowCount: normalized.rowCount,
|
|
232
|
+
executionTime: normalized.executionTime,
|
|
233
|
+
sql: prepared.sql,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const getSchemaContextForAgent = async (question) => {
|
|
237
|
+
try {
|
|
238
|
+
const result = await executor.executeQuery(`SELECT table_schema, table_name, column_name, data_type
|
|
239
|
+
FROM information_schema.columns
|
|
240
|
+
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
|
241
|
+
ORDER BY table_schema, table_name, ordinal_position`, [], runtimeVariables({}), connection);
|
|
242
|
+
return buildAgentSchemaContext(question, result.rows);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
};
|
|
213
248
|
// SSE clients for /api/watch hot-reload
|
|
214
249
|
const sseClients = new Set();
|
|
215
250
|
// Watch notebooks/, workbooks/, semantic-layer/, and data/ dirs for changes
|
|
@@ -298,7 +333,7 @@ export async function startLocalServer(opts) {
|
|
|
298
333
|
?? 'No executable SQL found in block source.';
|
|
299
334
|
throw new Error(message);
|
|
300
335
|
}
|
|
301
|
-
const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver });
|
|
336
|
+
const plan = buildExecutionPlan({ id: 'block-studio', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver, tableMapping });
|
|
302
337
|
const sql = resolveProjectRelativeSqlPaths(semanticCompose?.sql ?? plan?.sql ?? executableSql, projectRoot, projectConfig.dataDir);
|
|
303
338
|
const result = await executor.executeQuery(sql, plan?.sqlParams ?? [], runtimeVariables(plan?.variables ?? {}), targetConnection);
|
|
304
339
|
return {
|
|
@@ -309,7 +344,8 @@ export async function startLocalServer(opts) {
|
|
|
309
344
|
};
|
|
310
345
|
const runBlockStudioTestSummary = async (source, targetConnection = connection) => {
|
|
311
346
|
const start = Date.now();
|
|
312
|
-
const
|
|
347
|
+
const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
|
|
348
|
+
const plan = buildExecutionPlan({ id: 'block-studio-tests', type: 'dql', source, title: 'Block Studio' }, { semanticLayer, driver: targetConnection.driver, tableMapping });
|
|
313
349
|
const tests = plan?.tests ?? [];
|
|
314
350
|
if (!plan || !plan.sql) {
|
|
315
351
|
return {
|
|
@@ -578,7 +614,7 @@ export async function startLocalServer(opts) {
|
|
|
578
614
|
? body.variables
|
|
579
615
|
: {};
|
|
580
616
|
const tiles = [];
|
|
581
|
-
|
|
617
|
+
let localApps = null;
|
|
582
618
|
for (const item of loaded.dashboard.layout.items) {
|
|
583
619
|
if (item.text) {
|
|
584
620
|
tiles.push({
|
|
@@ -592,6 +628,18 @@ export async function startLocalServer(opts) {
|
|
|
592
628
|
continue;
|
|
593
629
|
}
|
|
594
630
|
if (item.aiPin) {
|
|
631
|
+
try {
|
|
632
|
+
localApps ??= new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
tiles.push({
|
|
636
|
+
tileId: item.i,
|
|
637
|
+
status: 'error',
|
|
638
|
+
tileType: 'aiPin',
|
|
639
|
+
error: err instanceof Error ? err.message : String(err),
|
|
640
|
+
});
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
595
643
|
let pin = localApps.getAiPin(item.aiPin.id);
|
|
596
644
|
if (!pin) {
|
|
597
645
|
tiles.push({
|
|
@@ -645,15 +693,18 @@ export async function startLocalServer(opts) {
|
|
|
645
693
|
});
|
|
646
694
|
const absBlockPath = join(projectRoot, block.filePath);
|
|
647
695
|
const source = readFileSync(absBlockPath, 'utf-8');
|
|
696
|
+
const targetConnection = isConnectionConfig(body.connection) ? body.connection : connection;
|
|
697
|
+
const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
|
|
648
698
|
const semanticCompose = semanticLayer
|
|
649
699
|
? composeSemanticBlockSql(source, semanticLayer, {
|
|
650
|
-
driver:
|
|
700
|
+
driver: targetConnection.driver,
|
|
701
|
+
tableMapping,
|
|
651
702
|
projectRoot,
|
|
652
703
|
projectConfig,
|
|
653
704
|
detectedProvider: semanticDetectedProvider,
|
|
654
705
|
})
|
|
655
706
|
: null;
|
|
656
|
-
const plan = buildExecutionPlan({ id: item.i, type: 'dql', source, title: item.title ?? block.name }, { semanticLayer, driver:
|
|
707
|
+
const plan = buildExecutionPlan({ id: item.i, type: 'dql', source, title: item.title ?? block.name }, { semanticLayer, driver: targetConnection.driver, tableMapping });
|
|
657
708
|
if (!plan && !semanticCompose?.sql) {
|
|
658
709
|
tiles.push({
|
|
659
710
|
tileId: item.i,
|
|
@@ -663,7 +714,7 @@ export async function startLocalServer(opts) {
|
|
|
663
714
|
});
|
|
664
715
|
continue;
|
|
665
716
|
}
|
|
666
|
-
const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql,
|
|
717
|
+
const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, targetConnection, projectRoot, projectConfig);
|
|
667
718
|
const result = await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...variables }), prepared.connection);
|
|
668
719
|
tiles.push({
|
|
669
720
|
tileId: item.i,
|
|
@@ -701,7 +752,7 @@ export async function startLocalServer(opts) {
|
|
|
701
752
|
}
|
|
702
753
|
}
|
|
703
754
|
}
|
|
704
|
-
localApps
|
|
755
|
+
localApps?.close();
|
|
705
756
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
706
757
|
res.end(serializeJSON({
|
|
707
758
|
appId,
|
|
@@ -1758,8 +1809,9 @@ export async function startLocalServer(opts) {
|
|
|
1758
1809
|
const defaultKey = raw.defaultConnection
|
|
1759
1810
|
? 'default'
|
|
1760
1811
|
: Object.keys(connections)[0] ?? 'default';
|
|
1812
|
+
const dbtProfiles = discoverDbtProfileConnections(projectRoot, cfg);
|
|
1761
1813
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1762
|
-
res.end(serializeJSON({ default: defaultKey, connections }));
|
|
1814
|
+
res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles }));
|
|
1763
1815
|
return;
|
|
1764
1816
|
}
|
|
1765
1817
|
// Save/update connections
|
|
@@ -2476,7 +2528,15 @@ export async function startLocalServer(opts) {
|
|
|
2476
2528
|
req.on('close', () => controller.abort());
|
|
2477
2529
|
const emit = (turn) => { res.write(`data: ${JSON.stringify(turn)}\n\n`); };
|
|
2478
2530
|
try {
|
|
2479
|
-
await runner.run({
|
|
2531
|
+
await runner.run({
|
|
2532
|
+
provider: resolvedProvider,
|
|
2533
|
+
messages,
|
|
2534
|
+
upstream,
|
|
2535
|
+
projectRoot,
|
|
2536
|
+
executeCertifiedBlock: executeCertifiedBlockForAgent,
|
|
2537
|
+
executeGeneratedSql: executeGeneratedSqlForAgent,
|
|
2538
|
+
getSchemaContext: getSchemaContextForAgent,
|
|
2539
|
+
}, emit, controller.signal);
|
|
2480
2540
|
}
|
|
2481
2541
|
catch (err) {
|
|
2482
2542
|
emit({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
@@ -3048,7 +3108,8 @@ export async function startLocalServer(opts) {
|
|
|
3048
3108
|
const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
|
|
3049
3109
|
const executableCell = resolved.cell;
|
|
3050
3110
|
const cellConnection = isConnectionConfig(body.connection) ? body.connection : connection;
|
|
3051
|
-
const
|
|
3111
|
+
const tableMapping = await resolveSemanticTableMapping(executor, cellConnection, semanticLayer);
|
|
3112
|
+
const plan = buildExecutionPlan(executableCell, { semanticLayer, driver: cellConnection.driver, tableMapping });
|
|
3052
3113
|
if (!plan) {
|
|
3053
3114
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3054
3115
|
res.end(serializeJSON({ cellType: cell.type, result: null }));
|
|
@@ -3308,6 +3369,7 @@ export function loadProjectConfig(projectRoot) {
|
|
|
3308
3369
|
// Support both `filepath` (correct) and `path` (legacy/init compat)
|
|
3309
3370
|
const filepath = (defaultConn.filepath ?? defaultConn.path);
|
|
3310
3371
|
config.defaultConnection = {
|
|
3372
|
+
...defaultConn,
|
|
3311
3373
|
driver: defaultConn.driver,
|
|
3312
3374
|
...(filepath ? { filepath } : {}),
|
|
3313
3375
|
};
|
|
@@ -3324,6 +3386,100 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
|
|
|
3324
3386
|
connection: normalizedConnection,
|
|
3325
3387
|
};
|
|
3326
3388
|
}
|
|
3389
|
+
const AGENT_PREVIEW_FORBIDDEN_SQL = [
|
|
3390
|
+
'alter',
|
|
3391
|
+
'analyze',
|
|
3392
|
+
'attach',
|
|
3393
|
+
'call',
|
|
3394
|
+
'copy',
|
|
3395
|
+
'create',
|
|
3396
|
+
'delete',
|
|
3397
|
+
'detach',
|
|
3398
|
+
'drop',
|
|
3399
|
+
'export',
|
|
3400
|
+
'grant',
|
|
3401
|
+
'import',
|
|
3402
|
+
'insert',
|
|
3403
|
+
'install',
|
|
3404
|
+
'load',
|
|
3405
|
+
'merge',
|
|
3406
|
+
'pragma',
|
|
3407
|
+
'reset',
|
|
3408
|
+
'revoke',
|
|
3409
|
+
'set',
|
|
3410
|
+
'truncate',
|
|
3411
|
+
'update',
|
|
3412
|
+
'vacuum',
|
|
3413
|
+
];
|
|
3414
|
+
export function buildAgentPreviewSql(sql) {
|
|
3415
|
+
const trimmed = sql.trim();
|
|
3416
|
+
if (!trimmed)
|
|
3417
|
+
throw new Error('Generated SQL preview is empty.');
|
|
3418
|
+
const withoutTrailingSemicolon = trimmed.replace(/;\s*$/, '').trim();
|
|
3419
|
+
const scanSql = stripSqlStringsAndComments(withoutTrailingSemicolon).trim();
|
|
3420
|
+
if (!/^(select|with)\b/i.test(scanSql)) {
|
|
3421
|
+
throw new Error('Generated SQL preview only supports read-only SELECT or WITH queries.');
|
|
3422
|
+
}
|
|
3423
|
+
if (scanSql.includes(';')) {
|
|
3424
|
+
throw new Error('Generated SQL preview only supports one statement.');
|
|
3425
|
+
}
|
|
3426
|
+
const forbiddenPattern = new RegExp(`\\b(${AGENT_PREVIEW_FORBIDDEN_SQL.join('|')})\\b`, 'i');
|
|
3427
|
+
const forbidden = scanSql.match(forbiddenPattern)?.[1];
|
|
3428
|
+
if (forbidden) {
|
|
3429
|
+
throw new Error(`Generated SQL preview rejected unsupported statement keyword: ${forbidden.toUpperCase()}.`);
|
|
3430
|
+
}
|
|
3431
|
+
return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT 200`;
|
|
3432
|
+
}
|
|
3433
|
+
function stripSqlStringsAndComments(sql) {
|
|
3434
|
+
let output = '';
|
|
3435
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
3436
|
+
const current = sql[index];
|
|
3437
|
+
const next = sql[index + 1];
|
|
3438
|
+
if (current === '-' && next === '-') {
|
|
3439
|
+
output += ' ';
|
|
3440
|
+
index += 2;
|
|
3441
|
+
while (index < sql.length && sql[index] !== '\n') {
|
|
3442
|
+
output += ' ';
|
|
3443
|
+
index += 1;
|
|
3444
|
+
}
|
|
3445
|
+
if (index < sql.length)
|
|
3446
|
+
output += '\n';
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
if (current === '/' && next === '*') {
|
|
3450
|
+
output += ' ';
|
|
3451
|
+
index += 2;
|
|
3452
|
+
while (index < sql.length && !(sql[index] === '*' && sql[index + 1] === '/')) {
|
|
3453
|
+
output += sql[index] === '\n' ? '\n' : ' ';
|
|
3454
|
+
index += 1;
|
|
3455
|
+
}
|
|
3456
|
+
if (index < sql.length) {
|
|
3457
|
+
output += ' ';
|
|
3458
|
+
index += 1;
|
|
3459
|
+
}
|
|
3460
|
+
continue;
|
|
3461
|
+
}
|
|
3462
|
+
if (current === "'" || current === '"') {
|
|
3463
|
+
const quote = current;
|
|
3464
|
+
output += ' ';
|
|
3465
|
+
while (index + 1 < sql.length) {
|
|
3466
|
+
index += 1;
|
|
3467
|
+
output += sql[index] === '\n' ? '\n' : ' ';
|
|
3468
|
+
if (sql[index] === quote) {
|
|
3469
|
+
if (sql[index + 1] === quote) {
|
|
3470
|
+
index += 1;
|
|
3471
|
+
output += ' ';
|
|
3472
|
+
continue;
|
|
3473
|
+
}
|
|
3474
|
+
break;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
continue;
|
|
3478
|
+
}
|
|
3479
|
+
output += current;
|
|
3480
|
+
}
|
|
3481
|
+
return output;
|
|
3482
|
+
}
|
|
3327
3483
|
/**
|
|
3328
3484
|
* Shared resolver for `@metric(name)` / `@dim(name)` refs in raw SQL.
|
|
3329
3485
|
* Used by notebook SQL execution and Block Studio validation so both paths
|
|
@@ -3344,7 +3500,7 @@ export function prepareSemanticSql(sql, semanticLayer) {
|
|
|
3344
3500
|
};
|
|
3345
3501
|
}
|
|
3346
3502
|
export function normalizeProjectConnection(connection, projectRoot) {
|
|
3347
|
-
const normalized = { ...connection };
|
|
3503
|
+
const normalized = expandConnectionEnvPlaceholders({ ...connection });
|
|
3348
3504
|
if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
|
|
3349
3505
|
normalized.filepath = resolve(projectRoot, normalized.filepath);
|
|
3350
3506
|
}
|
|
@@ -3353,6 +3509,15 @@ export function normalizeProjectConnection(connection, projectRoot) {
|
|
|
3353
3509
|
}
|
|
3354
3510
|
return normalized;
|
|
3355
3511
|
}
|
|
3512
|
+
function expandConnectionEnvPlaceholders(connection) {
|
|
3513
|
+
const expanded = {};
|
|
3514
|
+
for (const [key, value] of Object.entries(connection)) {
|
|
3515
|
+
expanded[key] = typeof value === 'string'
|
|
3516
|
+
? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, envKey) => process.env[envKey] ?? match)
|
|
3517
|
+
: value;
|
|
3518
|
+
}
|
|
3519
|
+
return expanded;
|
|
3520
|
+
}
|
|
3356
3521
|
export function resolveProjectRelativeSqlPaths(sql, projectRoot, dataDir) {
|
|
3357
3522
|
const resolvedRoot = resolve(projectRoot);
|
|
3358
3523
|
const normalizedDataDir = typeof dataDir === 'string' && dataDir.trim().length > 0
|
|
@@ -3495,6 +3660,8 @@ function scanNotebookFiles(projectRoot) {
|
|
|
3495
3660
|
workbooks: 'workbook',
|
|
3496
3661
|
blocks: 'block',
|
|
3497
3662
|
dashboards: 'dashboard',
|
|
3663
|
+
terms: 'term',
|
|
3664
|
+
'business-views': 'business_view',
|
|
3498
3665
|
};
|
|
3499
3666
|
for (const [folder, type] of Object.entries(folderMap)) {
|
|
3500
3667
|
const dir = join(projectRoot, folder);
|
|
@@ -3516,8 +3683,9 @@ function scanNotebookFiles(projectRoot) {
|
|
|
3516
3683
|
continue;
|
|
3517
3684
|
if (!entry.name.endsWith('.dql') && !entry.name.endsWith('.dqlnb'))
|
|
3518
3685
|
continue;
|
|
3686
|
+
const fallbackName = entry.name.replace(/\.(dql|dqlnb)$/, '');
|
|
3519
3687
|
result.push({
|
|
3520
|
-
name:
|
|
3688
|
+
name: inferDqlArtifactName(fullPath, type, fallbackName),
|
|
3521
3689
|
path: relativePath,
|
|
3522
3690
|
type,
|
|
3523
3691
|
folder: relativeDir.split('/')[0] ?? relativeDir,
|
|
@@ -3527,6 +3695,27 @@ function scanNotebookFiles(projectRoot) {
|
|
|
3527
3695
|
catch { /* skip unreadable dirs */ }
|
|
3528
3696
|
}
|
|
3529
3697
|
}
|
|
3698
|
+
function inferDqlArtifactName(fullPath, type, fallbackName) {
|
|
3699
|
+
if (!fullPath.endsWith('.dql'))
|
|
3700
|
+
return fallbackName;
|
|
3701
|
+
const expectedKind = {
|
|
3702
|
+
block: 'BlockDecl',
|
|
3703
|
+
dashboard: 'Dashboard',
|
|
3704
|
+
term: 'TermDecl',
|
|
3705
|
+
business_view: 'BusinessViewDecl',
|
|
3706
|
+
};
|
|
3707
|
+
const kind = expectedKind[type];
|
|
3708
|
+
if (!kind)
|
|
3709
|
+
return fallbackName;
|
|
3710
|
+
try {
|
|
3711
|
+
const ast = new Parser(readFileSync(fullPath, 'utf-8')).parse();
|
|
3712
|
+
const statement = ast.statements.find((item) => item.kind === kind && typeof item.name === 'string');
|
|
3713
|
+
return statement?.name ?? fallbackName;
|
|
3714
|
+
}
|
|
3715
|
+
catch {
|
|
3716
|
+
return fallbackName;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3530
3719
|
function scanDataFiles(projectRoot) {
|
|
3531
3720
|
const dataDir = join(projectRoot, 'data');
|
|
3532
3721
|
if (!existsSync(dataDir))
|
|
@@ -3756,7 +3945,20 @@ function parseSemanticBlockConfig(source) {
|
|
|
3756
3945
|
limit: limitMatch ? Number.parseInt(limitMatch[1], 10) : undefined,
|
|
3757
3946
|
};
|
|
3758
3947
|
}
|
|
3759
|
-
function
|
|
3948
|
+
export async function resolveSemanticTableMapping(executor, connection, semanticLayer) {
|
|
3949
|
+
if (!semanticLayer)
|
|
3950
|
+
return undefined;
|
|
3951
|
+
try {
|
|
3952
|
+
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
|
|
3953
|
+
FROM information_schema.tables
|
|
3954
|
+
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')`, [], {}, connection);
|
|
3955
|
+
return buildSemanticTableMapping(semanticLayer, tablesResult.rows);
|
|
3956
|
+
}
|
|
3957
|
+
catch {
|
|
3958
|
+
return undefined;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
export function buildSemanticTableMapping(semanticLayer, rows) {
|
|
3760
3962
|
const dbTableNames = new Set();
|
|
3761
3963
|
const schemaQualified = new Map();
|
|
3762
3964
|
for (const row of rows) {
|
|
@@ -4569,7 +4771,10 @@ function buildSemanticBlockContent(options) {
|
|
|
4569
4771
|
if (options.tags && options.tags.length > 0) {
|
|
4570
4772
|
lines.push(` tags = [${options.tags.map((tag) => `"${escapeDqlString(tag)}"`).join(', ')}]`);
|
|
4571
4773
|
}
|
|
4572
|
-
if (options.metrics.length ===
|
|
4774
|
+
if (options.metrics.length === 0) {
|
|
4775
|
+
lines.push(' metric = ""');
|
|
4776
|
+
}
|
|
4777
|
+
else if (options.metrics.length === 1) {
|
|
4573
4778
|
lines.push(` metric = "${escapeDqlString(options.metrics[0])}"`);
|
|
4574
4779
|
}
|
|
4575
4780
|
else {
|
|
@@ -4578,6 +4783,9 @@ function buildSemanticBlockContent(options) {
|
|
|
4578
4783
|
if (options.dimensions.length > 0) {
|
|
4579
4784
|
lines.push(` dimensions = [${options.dimensions.map((dimension) => `"${escapeDqlString(dimension)}"`).join(', ')}]`);
|
|
4580
4785
|
}
|
|
4786
|
+
else {
|
|
4787
|
+
lines.push(' dimensions = []');
|
|
4788
|
+
}
|
|
4581
4789
|
if (options.timeDimension) {
|
|
4582
4790
|
lines.push(` time_dimension = "${escapeDqlString(options.timeDimension.name)}"`);
|
|
4583
4791
|
lines.push(` granularity = "${escapeDqlString(options.timeDimension.granularity)}"`);
|
|
@@ -4945,7 +5153,51 @@ function resolveDbtManifestPath(projectRoot) {
|
|
|
4945
5153
|
const candidate = join(projectRoot, 'target', 'manifest.json');
|
|
4946
5154
|
return existsSync(candidate) ? candidate : undefined;
|
|
4947
5155
|
}
|
|
4948
|
-
export function
|
|
5156
|
+
export function discoverDbtProfileConnections(projectRoot, projectConfig) {
|
|
5157
|
+
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
5158
|
+
const projectProfileName = readDbtProjectProfileName(dbtProjectPath);
|
|
5159
|
+
const profilePaths = findDbtProfilePaths(projectRoot, dbtProjectPath);
|
|
5160
|
+
const candidates = [];
|
|
5161
|
+
for (const profilePath of profilePaths) {
|
|
5162
|
+
const profiles = readYamlFile(profilePath);
|
|
5163
|
+
if (!profiles)
|
|
5164
|
+
continue;
|
|
5165
|
+
for (const [profileName, rawProfile] of Object.entries(profiles)) {
|
|
5166
|
+
if (!rawProfile || typeof rawProfile !== 'object')
|
|
5167
|
+
continue;
|
|
5168
|
+
if (projectProfileName && profileName !== projectProfileName)
|
|
5169
|
+
continue;
|
|
5170
|
+
const profile = rawProfile;
|
|
5171
|
+
const outputs = profile.outputs && typeof profile.outputs === 'object'
|
|
5172
|
+
? profile.outputs
|
|
5173
|
+
: {};
|
|
5174
|
+
const defaultTarget = typeof profile.target === 'string' ? profile.target : 'default';
|
|
5175
|
+
for (const [targetName, output] of Object.entries(outputs)) {
|
|
5176
|
+
if (!output || typeof output !== 'object')
|
|
5177
|
+
continue;
|
|
5178
|
+
const mapped = mapDbtProfileOutput(output);
|
|
5179
|
+
if (!mapped)
|
|
5180
|
+
continue;
|
|
5181
|
+
const warnings = [...mapped.warnings];
|
|
5182
|
+
if (targetName !== defaultTarget) {
|
|
5183
|
+
warnings.push(`Not the default dbt target "${defaultTarget}".`);
|
|
5184
|
+
}
|
|
5185
|
+
candidates.push({
|
|
5186
|
+
id: `${profilePath}:${profileName}:${targetName}`,
|
|
5187
|
+
profileName,
|
|
5188
|
+
targetName,
|
|
5189
|
+
adapter: mapped.adapter,
|
|
5190
|
+
path: profilePath,
|
|
5191
|
+
connection: mapped.connection,
|
|
5192
|
+
missingFields: requiredConnectionFields(mapped.connection, mapped.envRefs),
|
|
5193
|
+
warnings,
|
|
5194
|
+
});
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
return candidates.slice(0, 20);
|
|
5199
|
+
}
|
|
5200
|
+
function findDbtProjectPath(projectRoot, projectConfig) {
|
|
4949
5201
|
const configuredDbtDir = projectConfig.dbt?.projectDir
|
|
4950
5202
|
? resolve(projectRoot, projectConfig.dbt.projectDir)
|
|
4951
5203
|
: undefined;
|
|
@@ -4960,7 +5212,259 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
|
4960
5212
|
resolve(projectRoot, '../dbt'),
|
|
4961
5213
|
resolve(projectRoot, '../../dbt'),
|
|
4962
5214
|
].filter((value) => Boolean(value));
|
|
4963
|
-
|
|
5215
|
+
return candidateDirs.find((dir, index, list) => list.indexOf(dir) === index && existsSync(join(dir, 'dbt_project.yml')))
|
|
5216
|
+
?? configuredDbtDir
|
|
5217
|
+
?? semanticDbtDir
|
|
5218
|
+
?? projectRoot;
|
|
5219
|
+
}
|
|
5220
|
+
function findDbtProfilePaths(projectRoot, dbtProjectPath) {
|
|
5221
|
+
const dirs = [
|
|
5222
|
+
process.env.DBT_PROFILES_DIR,
|
|
5223
|
+
dbtProjectPath,
|
|
5224
|
+
projectRoot,
|
|
5225
|
+
join(homedir(), '.dbt'),
|
|
5226
|
+
].filter((value) => Boolean(value));
|
|
5227
|
+
const paths = [];
|
|
5228
|
+
for (const dir of dirs) {
|
|
5229
|
+
for (const filename of ['profiles.yml', 'profiles.yaml']) {
|
|
5230
|
+
const profilePath = resolve(dir, filename);
|
|
5231
|
+
if (existsSync(profilePath) && !paths.includes(profilePath)) {
|
|
5232
|
+
paths.push(profilePath);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
return paths;
|
|
5237
|
+
}
|
|
5238
|
+
function readDbtProjectProfileName(dbtProjectPath) {
|
|
5239
|
+
const projectFile = join(dbtProjectPath, 'dbt_project.yml');
|
|
5240
|
+
const projectYaml = readYamlFile(projectFile);
|
|
5241
|
+
return typeof projectYaml?.profile === 'string' ? projectYaml.profile : null;
|
|
5242
|
+
}
|
|
5243
|
+
function readYamlFile(path) {
|
|
5244
|
+
if (!existsSync(path))
|
|
5245
|
+
return null;
|
|
5246
|
+
try {
|
|
5247
|
+
const parsed = loadYaml(readFileSync(path, 'utf-8'));
|
|
5248
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
5249
|
+
? parsed
|
|
5250
|
+
: null;
|
|
5251
|
+
}
|
|
5252
|
+
catch {
|
|
5253
|
+
return null;
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
function mapDbtProfileOutput(output) {
|
|
5257
|
+
const adapter = text(output, 'type').value?.toLowerCase();
|
|
5258
|
+
const envRefs = new Set();
|
|
5259
|
+
const warnings = [];
|
|
5260
|
+
const read = (...keys) => {
|
|
5261
|
+
const result = text(output, ...keys);
|
|
5262
|
+
result.envRefs.forEach((ref) => envRefs.add(ref));
|
|
5263
|
+
return result.value;
|
|
5264
|
+
};
|
|
5265
|
+
const port = numberValue(output, 'port');
|
|
5266
|
+
const sslRaw = read('ssl', 'sslmode');
|
|
5267
|
+
const ssl = sslRaw === undefined
|
|
5268
|
+
? undefined
|
|
5269
|
+
: !['false', '0', 'disable', 'disabled', 'off'].includes(sslRaw.toLowerCase());
|
|
5270
|
+
switch (adapter) {
|
|
5271
|
+
case 'postgres':
|
|
5272
|
+
case 'postgresql':
|
|
5273
|
+
return {
|
|
5274
|
+
adapter,
|
|
5275
|
+
connection: compactConnection({
|
|
5276
|
+
driver: 'postgresql',
|
|
5277
|
+
host: read('host'),
|
|
5278
|
+
port,
|
|
5279
|
+
database: read('dbname', 'database'),
|
|
5280
|
+
schema: read('schema'),
|
|
5281
|
+
username: read('user', 'username'),
|
|
5282
|
+
password: read('password', 'pass'),
|
|
5283
|
+
ssl,
|
|
5284
|
+
}),
|
|
5285
|
+
envRefs: [...envRefs],
|
|
5286
|
+
warnings,
|
|
5287
|
+
};
|
|
5288
|
+
case 'redshift':
|
|
5289
|
+
return {
|
|
5290
|
+
adapter,
|
|
5291
|
+
connection: compactConnection({
|
|
5292
|
+
driver: 'redshift',
|
|
5293
|
+
host: read('host'),
|
|
5294
|
+
port: port ?? 5439,
|
|
5295
|
+
database: read('dbname', 'database'),
|
|
5296
|
+
schema: read('schema'),
|
|
5297
|
+
username: read('user', 'username'),
|
|
5298
|
+
password: read('password', 'pass'),
|
|
5299
|
+
ssl,
|
|
5300
|
+
}),
|
|
5301
|
+
envRefs: [...envRefs],
|
|
5302
|
+
warnings,
|
|
5303
|
+
};
|
|
5304
|
+
case 'snowflake': {
|
|
5305
|
+
const privateKeyPath = read('private_key_path', 'privateKeyPath');
|
|
5306
|
+
const authenticator = read('authenticator');
|
|
5307
|
+
const authMethod = privateKeyPath
|
|
5308
|
+
? 'key_pair'
|
|
5309
|
+
: authenticator?.toLowerCase() === 'externalbrowser'
|
|
5310
|
+
? 'external_browser'
|
|
5311
|
+
: 'password';
|
|
5312
|
+
return {
|
|
5313
|
+
adapter,
|
|
5314
|
+
connection: compactConnection({
|
|
5315
|
+
driver: 'snowflake',
|
|
5316
|
+
account: read('account'),
|
|
5317
|
+
warehouse: read('warehouse'),
|
|
5318
|
+
database: read('database'),
|
|
5319
|
+
schema: read('schema'),
|
|
5320
|
+
username: read('user', 'username'),
|
|
5321
|
+
password: read('password'),
|
|
5322
|
+
role: read('role'),
|
|
5323
|
+
privateKeyPath,
|
|
5324
|
+
privateKeyPassphrase: read('private_key_passphrase', 'privateKeyPassphrase'),
|
|
5325
|
+
authenticator,
|
|
5326
|
+
authMethod,
|
|
5327
|
+
}),
|
|
5328
|
+
envRefs: [...envRefs],
|
|
5329
|
+
warnings,
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
case 'bigquery': {
|
|
5333
|
+
const keyFilename = read('keyfile', 'keyFilename');
|
|
5334
|
+
return {
|
|
5335
|
+
adapter,
|
|
5336
|
+
connection: compactConnection({
|
|
5337
|
+
driver: 'bigquery',
|
|
5338
|
+
projectId: read('project', 'projectId'),
|
|
5339
|
+
schema: read('dataset', 'schema'),
|
|
5340
|
+
location: read('location'),
|
|
5341
|
+
keyFilename,
|
|
5342
|
+
authMethod: keyFilename ? 'service_account_key_file' : 'application_default',
|
|
5343
|
+
}),
|
|
5344
|
+
envRefs: [...envRefs],
|
|
5345
|
+
warnings,
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
case 'duckdb':
|
|
5349
|
+
return {
|
|
5350
|
+
adapter,
|
|
5351
|
+
connection: compactConnection({
|
|
5352
|
+
driver: 'duckdb',
|
|
5353
|
+
filepath: read('path', 'database') ?? ':memory:',
|
|
5354
|
+
}),
|
|
5355
|
+
envRefs: [...envRefs],
|
|
5356
|
+
warnings,
|
|
5357
|
+
};
|
|
5358
|
+
case 'databricks':
|
|
5359
|
+
return {
|
|
5360
|
+
adapter,
|
|
5361
|
+
connection: compactConnection({
|
|
5362
|
+
driver: 'databricks',
|
|
5363
|
+
host: read('host', 'server_hostname'),
|
|
5364
|
+
httpPath: read('http_path', 'httpPath'),
|
|
5365
|
+
warehouse: read('warehouse', 'warehouse_id'),
|
|
5366
|
+
catalog: read('catalog'),
|
|
5367
|
+
database: read('catalog', 'database'),
|
|
5368
|
+
schema: read('schema'),
|
|
5369
|
+
token: read('token'),
|
|
5370
|
+
authMethod: 'token',
|
|
5371
|
+
}),
|
|
5372
|
+
envRefs: [...envRefs],
|
|
5373
|
+
warnings,
|
|
5374
|
+
};
|
|
5375
|
+
default:
|
|
5376
|
+
return null;
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
function text(source, ...keys) {
|
|
5380
|
+
for (const key of keys) {
|
|
5381
|
+
const raw = source[key];
|
|
5382
|
+
if (raw === undefined || raw === null)
|
|
5383
|
+
continue;
|
|
5384
|
+
const value = String(raw).trim();
|
|
5385
|
+
if (!value)
|
|
5386
|
+
continue;
|
|
5387
|
+
return resolveDbtEnvVars(value);
|
|
5388
|
+
}
|
|
5389
|
+
return { envRefs: [] };
|
|
5390
|
+
}
|
|
5391
|
+
function resolveDbtEnvVars(value) {
|
|
5392
|
+
const envRefs = [];
|
|
5393
|
+
const replaced = value.replace(/\{\{\s*env_var\(\s*['"]([^'"]+)['"]\s*(?:,\s*(['"])(.*?)\2)?\s*\)\s*\}\}/g, (_match, envKey, _quote, fallback) => {
|
|
5394
|
+
const envValue = process.env[envKey];
|
|
5395
|
+
if (envValue !== undefined)
|
|
5396
|
+
return envValue;
|
|
5397
|
+
if (fallback !== undefined)
|
|
5398
|
+
return fallback;
|
|
5399
|
+
envRefs.push(envKey);
|
|
5400
|
+
return `\${${envKey}}`;
|
|
5401
|
+
});
|
|
5402
|
+
return { value: replaced, envRefs };
|
|
5403
|
+
}
|
|
5404
|
+
function numberValue(source, key) {
|
|
5405
|
+
const raw = source[key];
|
|
5406
|
+
if (raw === undefined || raw === null || raw === '')
|
|
5407
|
+
return undefined;
|
|
5408
|
+
const value = Number(raw);
|
|
5409
|
+
return Number.isFinite(value) ? value : undefined;
|
|
5410
|
+
}
|
|
5411
|
+
function compactConnection(connection) {
|
|
5412
|
+
const compact = {};
|
|
5413
|
+
for (const [key, value] of Object.entries(connection)) {
|
|
5414
|
+
if (value === undefined || value === null || value === '')
|
|
5415
|
+
continue;
|
|
5416
|
+
compact[key] = value;
|
|
5417
|
+
}
|
|
5418
|
+
return compact;
|
|
5419
|
+
}
|
|
5420
|
+
function requiredConnectionFields(connection, envRefs) {
|
|
5421
|
+
const missing = new Set();
|
|
5422
|
+
const needs = (field) => {
|
|
5423
|
+
const value = connection[field];
|
|
5424
|
+
if (value === undefined || value === null || value === '')
|
|
5425
|
+
missing.add(String(field));
|
|
5426
|
+
};
|
|
5427
|
+
switch (connection.driver) {
|
|
5428
|
+
case 'postgresql':
|
|
5429
|
+
case 'redshift':
|
|
5430
|
+
needs('host');
|
|
5431
|
+
needs('database');
|
|
5432
|
+
needs('username');
|
|
5433
|
+
break;
|
|
5434
|
+
case 'snowflake':
|
|
5435
|
+
needs('account');
|
|
5436
|
+
needs('warehouse');
|
|
5437
|
+
needs('database');
|
|
5438
|
+
needs('schema');
|
|
5439
|
+
needs('username');
|
|
5440
|
+
if (connection.authMethod === 'key_pair') {
|
|
5441
|
+
needs('privateKeyPath');
|
|
5442
|
+
}
|
|
5443
|
+
else if (connection.authMethod !== 'external_browser') {
|
|
5444
|
+
needs('password');
|
|
5445
|
+
}
|
|
5446
|
+
break;
|
|
5447
|
+
case 'bigquery':
|
|
5448
|
+
needs('projectId');
|
|
5449
|
+
break;
|
|
5450
|
+
case 'duckdb':
|
|
5451
|
+
needs('filepath');
|
|
5452
|
+
break;
|
|
5453
|
+
case 'databricks':
|
|
5454
|
+
needs('host');
|
|
5455
|
+
if (!connection.httpPath && !connection.warehouse)
|
|
5456
|
+
missing.add('httpPath');
|
|
5457
|
+
needs('token');
|
|
5458
|
+
break;
|
|
5459
|
+
}
|
|
5460
|
+
for (const envKey of envRefs) {
|
|
5461
|
+
if (!process.env[envKey])
|
|
5462
|
+
missing.add(`env:${envKey}`);
|
|
5463
|
+
}
|
|
5464
|
+
return [...missing];
|
|
5465
|
+
}
|
|
5466
|
+
export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
5467
|
+
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
4964
5468
|
const configuredManifest = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
|
|
4965
5469
|
const manifestPath = resolve(dbtProjectPath, configuredManifest);
|
|
4966
5470
|
const catalogPath = resolve(dbtProjectPath, 'target/catalog.json');
|
|
@@ -4990,7 +5494,9 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
|
|
|
4990
5494
|
const savedQueryCount = Array.isArray(semanticManifest?.saved_queries)
|
|
4991
5495
|
? semanticManifest.saved_queries.length
|
|
4992
5496
|
: 0;
|
|
4993
|
-
const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
|
|
5497
|
+
const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
|
|
5498
|
+
|| Boolean(projectConfig.dbt?.projectDir)
|
|
5499
|
+
|| Boolean(projectConfig.semanticLayer?.provider === 'dbt' && projectConfig.semanticLayer.projectPath);
|
|
4994
5500
|
const manifestExists = existsSync(manifestPath);
|
|
4995
5501
|
const semanticExists = existsSync(semanticManifestPath);
|
|
4996
5502
|
const setupHint = !configured
|
|
@@ -5107,14 +5613,21 @@ async function execGit(cwd, args) {
|
|
|
5107
5613
|
});
|
|
5108
5614
|
});
|
|
5109
5615
|
}
|
|
5110
|
-
async function
|
|
5616
|
+
async function resolveGitRoot(cwd) {
|
|
5111
5617
|
const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
5112
|
-
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
|
|
5618
|
+
if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
|
|
5619
|
+
return null;
|
|
5620
|
+
const root = await execGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
5621
|
+
return root.code === 0 && root.stdout.trim() ? root.stdout.trim() : cwd;
|
|
5622
|
+
}
|
|
5623
|
+
async function readGitStatus(cwd) {
|
|
5624
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5625
|
+
if (!gitRoot) {
|
|
5113
5626
|
return { inRepo: false, branch: null, ahead: 0, behind: 0, changes: [] };
|
|
5114
5627
|
}
|
|
5115
|
-
const branchRes = await execGit(
|
|
5628
|
+
const branchRes = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5116
5629
|
const branch = branchRes.code === 0 ? branchRes.stdout.trim() : null;
|
|
5117
|
-
const trackRes = await execGit(
|
|
5630
|
+
const trackRes = await execGit(gitRoot, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
|
|
5118
5631
|
let ahead = 0;
|
|
5119
5632
|
let behind = 0;
|
|
5120
5633
|
if (trackRes.code === 0) {
|
|
@@ -5122,7 +5635,7 @@ async function readGitStatus(cwd) {
|
|
|
5122
5635
|
behind = Number(match[0] ?? 0);
|
|
5123
5636
|
ahead = Number(match[1] ?? 0);
|
|
5124
5637
|
}
|
|
5125
|
-
const statusRes = await execGit(
|
|
5638
|
+
const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=normal']);
|
|
5126
5639
|
const changes = [];
|
|
5127
5640
|
if (statusRes.code === 0) {
|
|
5128
5641
|
for (const line of statusRes.stdout.split('\n')) {
|
|
@@ -5136,13 +5649,13 @@ async function readGitStatus(cwd) {
|
|
|
5136
5649
|
return { inRepo: true, branch, ahead, behind, changes };
|
|
5137
5650
|
}
|
|
5138
5651
|
async function readGitLog(cwd, limit) {
|
|
5139
|
-
const
|
|
5140
|
-
if (
|
|
5652
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5653
|
+
if (!gitRoot)
|
|
5141
5654
|
return { inRepo: false, commits: [] };
|
|
5142
5655
|
const sep = '\x1f';
|
|
5143
5656
|
const end = '\x1e';
|
|
5144
5657
|
const fmt = ['%H', '%an', '%ad', '%s'].join(sep) + end;
|
|
5145
|
-
const res = await execGit(
|
|
5658
|
+
const res = await execGit(gitRoot, ['log', `-${limit}`, `--pretty=format:${fmt}`, '--date=short']);
|
|
5146
5659
|
if (res.code !== 0)
|
|
5147
5660
|
return { inRepo: true, commits: [] };
|
|
5148
5661
|
const commits = [];
|
|
@@ -5205,23 +5718,97 @@ function ensureGitignoreEntry(projectRoot, pattern) {
|
|
|
5205
5718
|
}
|
|
5206
5719
|
}
|
|
5207
5720
|
async function readGitDiff(cwd, filePath, staged = false) {
|
|
5208
|
-
const
|
|
5209
|
-
if (
|
|
5721
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5722
|
+
if (!gitRoot) {
|
|
5210
5723
|
return { inRepo: false, diff: '', before: null, after: null, diffReport: null };
|
|
5211
5724
|
}
|
|
5212
5725
|
const baseArgs = staged ? ['diff', '--cached', '--no-color'] : ['diff', '--no-color'];
|
|
5213
5726
|
if (!filePath) {
|
|
5214
|
-
const res = await execGit(
|
|
5727
|
+
const res = await execGit(gitRoot, baseArgs);
|
|
5215
5728
|
return { inRepo: true, diff: res.stdout, before: null, after: null, diffReport: null };
|
|
5216
5729
|
}
|
|
5217
5730
|
const isSemantic = filePath.endsWith('.dql') || filePath.endsWith('.dqlnb');
|
|
5218
5731
|
const [diffRes, before, after] = await Promise.all([
|
|
5219
|
-
execGit(
|
|
5220
|
-
isSemantic ? readHeadBlob(
|
|
5221
|
-
isSemantic ? readWorkingCopy(join(
|
|
5732
|
+
execGit(gitRoot, [...baseArgs, '--', filePath]),
|
|
5733
|
+
isSemantic ? readHeadBlob(gitRoot, filePath) : Promise.resolve(null),
|
|
5734
|
+
isSemantic ? readWorkingCopy(join(gitRoot, filePath)) : Promise.resolve(null),
|
|
5222
5735
|
]);
|
|
5736
|
+
const diffText = !staged && !diffRes.stdout.trim()
|
|
5737
|
+
? (await readUntrackedTextDiff(gitRoot, filePath)) || diffRes.stdout
|
|
5738
|
+
: diffRes.stdout;
|
|
5223
5739
|
const diffReport = isSemantic ? computeSemanticDiff(filePath, before, after) : null;
|
|
5224
|
-
return { inRepo: true, diff:
|
|
5740
|
+
return { inRepo: true, diff: diffText, before, after, diffReport };
|
|
5741
|
+
}
|
|
5742
|
+
const MAX_UNTRACKED_DIFF_FILES = 20;
|
|
5743
|
+
const MAX_UNTRACKED_DIFF_BYTES = 512 * 1024;
|
|
5744
|
+
async function readUntrackedTextDiff(cwd, filePath) {
|
|
5745
|
+
const status = await execGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '--', filePath]);
|
|
5746
|
+
if (status.code !== 0 || !status.stdout.split('\n').some((line) => line.startsWith('?? '))) {
|
|
5747
|
+
return '';
|
|
5748
|
+
}
|
|
5749
|
+
const listed = await execGit(cwd, ['ls-files', '--others', '--exclude-standard', '--', filePath]);
|
|
5750
|
+
if (listed.code !== 0)
|
|
5751
|
+
return '';
|
|
5752
|
+
const chunks = [];
|
|
5753
|
+
let totalBytes = 0;
|
|
5754
|
+
for (const rawPath of listed.stdout.split('\n').map((p) => p.trim()).filter(Boolean)) {
|
|
5755
|
+
if (chunks.length >= MAX_UNTRACKED_DIFF_FILES || totalBytes >= MAX_UNTRACKED_DIFF_BYTES)
|
|
5756
|
+
break;
|
|
5757
|
+
const absPath = safeJoin(cwd, rawPath);
|
|
5758
|
+
if (!absPath || !existsSync(absPath))
|
|
5759
|
+
continue;
|
|
5760
|
+
const st = statSync(absPath);
|
|
5761
|
+
if (!st.isFile())
|
|
5762
|
+
continue;
|
|
5763
|
+
if (st.size > MAX_UNTRACKED_DIFF_BYTES) {
|
|
5764
|
+
chunks.push(formatBinaryAddedDiff(rawPath));
|
|
5765
|
+
continue;
|
|
5766
|
+
}
|
|
5767
|
+
const buf = readFileSync(absPath);
|
|
5768
|
+
if (buf.includes(0)) {
|
|
5769
|
+
chunks.push(formatBinaryAddedDiff(rawPath));
|
|
5770
|
+
continue;
|
|
5771
|
+
}
|
|
5772
|
+
totalBytes += buf.length;
|
|
5773
|
+
chunks.push(formatAddedFileDiff(rawPath, buf.toString('utf-8')));
|
|
5774
|
+
}
|
|
5775
|
+
if (chunks.length === 0)
|
|
5776
|
+
return '';
|
|
5777
|
+
const omitted = listed.stdout.split('\n').filter(Boolean).length - chunks.length;
|
|
5778
|
+
if (omitted > 0) {
|
|
5779
|
+
chunks.push(`diff --git a/${filePath} b/${filePath}\n# ${omitted} additional untracked file${omitted === 1 ? '' : 's'} omitted from preview`);
|
|
5780
|
+
}
|
|
5781
|
+
return chunks.join('\n');
|
|
5782
|
+
}
|
|
5783
|
+
function formatAddedFileDiff(filePath, content) {
|
|
5784
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
5785
|
+
const hasFinalNewline = normalized.endsWith('\n');
|
|
5786
|
+
const lines = normalized.length === 0
|
|
5787
|
+
? []
|
|
5788
|
+
: normalized.split('\n').slice(0, hasFinalNewline ? -1 : undefined);
|
|
5789
|
+
const hunk = lines.length > 0
|
|
5790
|
+
? [`@@ -0,0 +1,${lines.length} @@`, ...lines.map((line) => `+${line}`)]
|
|
5791
|
+
: [];
|
|
5792
|
+
if (!hasFinalNewline && normalized.length > 0)
|
|
5793
|
+
hunk.push('\');
|
|
5794
|
+
return [
|
|
5795
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
5796
|
+
'new file mode 100644',
|
|
5797
|
+
'index 0000000..0000000',
|
|
5798
|
+
'--- /dev/null',
|
|
5799
|
+
`+++ b/${filePath}`,
|
|
5800
|
+
...hunk,
|
|
5801
|
+
].join('\n');
|
|
5802
|
+
}
|
|
5803
|
+
function formatBinaryAddedDiff(filePath) {
|
|
5804
|
+
return [
|
|
5805
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
5806
|
+
'new file mode 100644',
|
|
5807
|
+
'index 0000000..0000000',
|
|
5808
|
+
'--- /dev/null',
|
|
5809
|
+
`+++ b/${filePath}`,
|
|
5810
|
+
`Binary file ${filePath} added`,
|
|
5811
|
+
].join('\n');
|
|
5225
5812
|
}
|
|
5226
5813
|
// ── git write operations ──────────────────────────────────────────────────
|
|
5227
5814
|
// Each helper validates inputs, shells out via execFile (no shell expansion),
|
|
@@ -5251,89 +5838,118 @@ function gitErrorOutput(res) {
|
|
|
5251
5838
|
return (res.stderr || res.stdout || '').trim();
|
|
5252
5839
|
}
|
|
5253
5840
|
async function gitStage(cwd, paths) {
|
|
5254
|
-
const
|
|
5841
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5842
|
+
if (!gitRoot)
|
|
5843
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5844
|
+
const v = validatePaths(gitRoot, paths);
|
|
5255
5845
|
if (!v.ok)
|
|
5256
5846
|
return { ok: false, error: v.error };
|
|
5257
|
-
const res = await execGit(
|
|
5847
|
+
const res = await execGit(gitRoot, ['add', '--', ...v.paths]);
|
|
5258
5848
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5259
5849
|
}
|
|
5260
5850
|
async function gitUnstage(cwd, paths) {
|
|
5261
|
-
const
|
|
5851
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5852
|
+
if (!gitRoot)
|
|
5853
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5854
|
+
const v = validatePaths(gitRoot, paths);
|
|
5262
5855
|
if (!v.ok)
|
|
5263
5856
|
return { ok: false, error: v.error };
|
|
5264
5857
|
// `restore --staged` works with or without HEAD; for an initial commit (no
|
|
5265
5858
|
// HEAD yet) git's `rm --cached` is the fallback. Try restore first.
|
|
5266
|
-
const res = await execGit(
|
|
5859
|
+
const res = await execGit(gitRoot, ['restore', '--staged', '--', ...v.paths]);
|
|
5267
5860
|
if (res.code === 0)
|
|
5268
5861
|
return { ok: true };
|
|
5269
|
-
const fallback = await execGit(
|
|
5862
|
+
const fallback = await execGit(gitRoot, ['rm', '--cached', '-r', '--', ...v.paths]);
|
|
5270
5863
|
return fallback.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(fallback) };
|
|
5271
5864
|
}
|
|
5272
5865
|
async function gitDiscard(cwd, paths) {
|
|
5273
|
-
const
|
|
5866
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5867
|
+
if (!gitRoot)
|
|
5868
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5869
|
+
const v = validatePaths(gitRoot, paths);
|
|
5274
5870
|
if (!v.ok)
|
|
5275
5871
|
return { ok: false, error: v.error };
|
|
5276
5872
|
// For tracked files: `restore --worktree` reverts to HEAD. For untracked
|
|
5277
5873
|
// files: that's a no-op and we delete them via `clean -f`. Run both so
|
|
5278
5874
|
// the caller doesn't have to know which list each path is in.
|
|
5279
|
-
const restore = await execGit(
|
|
5280
|
-
const clean = await execGit(
|
|
5875
|
+
const restore = await execGit(gitRoot, ['restore', '--worktree', '--', ...v.paths]);
|
|
5876
|
+
const clean = await execGit(gitRoot, ['clean', '-f', '--', ...v.paths]);
|
|
5281
5877
|
if (restore.code !== 0 && clean.code !== 0) {
|
|
5282
5878
|
return { ok: false, error: gitErrorOutput(restore) || gitErrorOutput(clean) };
|
|
5283
5879
|
}
|
|
5284
5880
|
return { ok: true };
|
|
5285
5881
|
}
|
|
5286
5882
|
async function gitCommit(cwd, message, stageAll) {
|
|
5883
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5884
|
+
if (!gitRoot)
|
|
5885
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5287
5886
|
const trimmed = message.trim();
|
|
5288
5887
|
if (!trimmed)
|
|
5289
5888
|
return { ok: false, error: 'Commit message required' };
|
|
5290
5889
|
if (stageAll) {
|
|
5291
|
-
const add = await execGit(
|
|
5890
|
+
const add = await execGit(gitRoot, ['add', '-A']);
|
|
5292
5891
|
if (add.code !== 0)
|
|
5293
5892
|
return { ok: false, error: gitErrorOutput(add) };
|
|
5294
5893
|
}
|
|
5295
|
-
const res = await execGit(
|
|
5894
|
+
const res = await execGit(gitRoot, ['commit', '-m', trimmed]);
|
|
5296
5895
|
if (res.code !== 0)
|
|
5297
5896
|
return { ok: false, error: gitErrorOutput(res) };
|
|
5298
|
-
const hashRes = await execGit(
|
|
5897
|
+
const hashRes = await execGit(gitRoot, ['rev-parse', 'HEAD']);
|
|
5299
5898
|
return { ok: true, hash: hashRes.code === 0 ? hashRes.stdout.trim() : undefined };
|
|
5300
5899
|
}
|
|
5301
5900
|
async function gitPush(cwd) {
|
|
5302
|
-
const
|
|
5901
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5902
|
+
if (!gitRoot)
|
|
5903
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5904
|
+
const branch = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5905
|
+
const upstream = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
5906
|
+
const remotes = await execGit(gitRoot, ['remote']);
|
|
5907
|
+
const remote = remotes.stdout.split('\n').map((s) => s.trim()).find(Boolean) ?? 'origin';
|
|
5908
|
+
const branchName = branch.code === 0 ? branch.stdout.trim() : '';
|
|
5909
|
+
const args = upstream.code === 0 || !branchName || branchName === 'HEAD'
|
|
5910
|
+
? ['push']
|
|
5911
|
+
: ['push', '-u', remote, branchName];
|
|
5912
|
+
const res = await execGit(gitRoot, args);
|
|
5303
5913
|
return res.code === 0
|
|
5304
5914
|
? { ok: true, output: gitErrorOutput(res) }
|
|
5305
5915
|
: { ok: false, error: gitErrorOutput(res) };
|
|
5306
5916
|
}
|
|
5307
5917
|
async function gitPull(cwd) {
|
|
5918
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5919
|
+
if (!gitRoot)
|
|
5920
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5308
5921
|
// `--ff-only` keeps the operation non-destructive: if the local branch has
|
|
5309
5922
|
// diverged from upstream, we surface the error rather than auto-merging.
|
|
5310
5923
|
// The user can resolve via the terminal or a future merge UI.
|
|
5311
|
-
const res = await execGit(
|
|
5924
|
+
const res = await execGit(gitRoot, ['pull', '--ff-only']);
|
|
5312
5925
|
return res.code === 0
|
|
5313
5926
|
? { ok: true, output: gitErrorOutput(res) }
|
|
5314
5927
|
: { ok: false, error: gitErrorOutput(res) };
|
|
5315
5928
|
}
|
|
5316
5929
|
async function readGitBranches(cwd) {
|
|
5317
|
-
const
|
|
5318
|
-
if (
|
|
5930
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5931
|
+
if (!gitRoot)
|
|
5319
5932
|
return { inRepo: false, current: null, branches: [] };
|
|
5320
|
-
const cur = await execGit(
|
|
5321
|
-
const list = await execGit(
|
|
5933
|
+
const cur = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
5934
|
+
const list = await execGit(gitRoot, ['branch', '--list', '--format=%(refname:short)']);
|
|
5322
5935
|
const branches = list.code === 0
|
|
5323
5936
|
? list.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
5324
5937
|
: [];
|
|
5325
5938
|
return { inRepo: true, current: cur.code === 0 ? cur.stdout.trim() : null, branches };
|
|
5326
5939
|
}
|
|
5327
5940
|
async function readGitRemote(cwd) {
|
|
5328
|
-
const
|
|
5329
|
-
if (
|
|
5941
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5942
|
+
if (!gitRoot)
|
|
5330
5943
|
return { inRepo: false, url: null, name: null };
|
|
5331
|
-
const remoteName = await execGit(
|
|
5944
|
+
const remoteName = await execGit(gitRoot, ['config', '--get', 'remote.pushDefault']);
|
|
5332
5945
|
const name = remoteName.code === 0 && remoteName.stdout.trim() ? remoteName.stdout.trim() : 'origin';
|
|
5333
|
-
const url = await execGit(
|
|
5946
|
+
const url = await execGit(gitRoot, ['remote', 'get-url', name]);
|
|
5334
5947
|
return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
|
|
5335
5948
|
}
|
|
5336
5949
|
async function gitCreateBranch(cwd, name, checkout) {
|
|
5950
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5951
|
+
if (!gitRoot)
|
|
5952
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5337
5953
|
const trimmed = name.trim();
|
|
5338
5954
|
// Branch names can't start with `-` (would be parsed as a flag) and must be
|
|
5339
5955
|
// non-empty. git itself enforces the rest of the ref-name rules.
|
|
@@ -5342,17 +5958,20 @@ async function gitCreateBranch(cwd, name, checkout) {
|
|
|
5342
5958
|
if (trimmed.startsWith('-'))
|
|
5343
5959
|
return { ok: false, error: 'Invalid branch name' };
|
|
5344
5960
|
const res = checkout
|
|
5345
|
-
? await execGit(
|
|
5346
|
-
: await execGit(
|
|
5961
|
+
? await execGit(gitRoot, ['checkout', '-b', trimmed])
|
|
5962
|
+
: await execGit(gitRoot, ['branch', trimmed]);
|
|
5347
5963
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5348
5964
|
}
|
|
5349
5965
|
async function gitCheckout(cwd, name) {
|
|
5966
|
+
const gitRoot = await resolveGitRoot(cwd);
|
|
5967
|
+
if (!gitRoot)
|
|
5968
|
+
return { ok: false, error: 'Not a git repository' };
|
|
5350
5969
|
const trimmed = name.trim();
|
|
5351
5970
|
if (!trimmed)
|
|
5352
5971
|
return { ok: false, error: 'Branch name required' };
|
|
5353
5972
|
if (trimmed.startsWith('-'))
|
|
5354
5973
|
return { ok: false, error: 'Invalid branch name' };
|
|
5355
|
-
const res = await execGit(
|
|
5974
|
+
const res = await execGit(gitRoot, ['checkout', trimmed]);
|
|
5356
5975
|
return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
|
|
5357
5976
|
}
|
|
5358
5977
|
async function readHeadBlob(cwd, filePath) {
|
|
@@ -5482,6 +6101,92 @@ function isAiPinRefreshDue(lastRefreshedAt) {
|
|
|
5482
6101
|
return true;
|
|
5483
6102
|
return Date.now() - last >= 24 * 60 * 60 * 1000;
|
|
5484
6103
|
}
|
|
6104
|
+
function buildAgentSchemaContext(question, rows) {
|
|
6105
|
+
const byRelation = new Map();
|
|
6106
|
+
for (const row of rows) {
|
|
6107
|
+
if (!row || typeof row !== 'object')
|
|
6108
|
+
continue;
|
|
6109
|
+
const record = row;
|
|
6110
|
+
const schema = stringFromRecord(record, 'table_schema');
|
|
6111
|
+
const table = stringFromRecord(record, 'table_name');
|
|
6112
|
+
const column = stringFromRecord(record, 'column_name');
|
|
6113
|
+
if (!schema || !table || !column)
|
|
6114
|
+
continue;
|
|
6115
|
+
const relation = `${schema}.${table}`;
|
|
6116
|
+
const current = byRelation.get(relation) ?? {
|
|
6117
|
+
relation,
|
|
6118
|
+
schema,
|
|
6119
|
+
name: table,
|
|
6120
|
+
source: 'runtime information_schema',
|
|
6121
|
+
columns: [],
|
|
6122
|
+
};
|
|
6123
|
+
if (current.columns.length < 80) {
|
|
6124
|
+
current.columns.push({
|
|
6125
|
+
name: column,
|
|
6126
|
+
type: stringFromRecord(record, 'data_type'),
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
6129
|
+
byRelation.set(relation, current);
|
|
6130
|
+
}
|
|
6131
|
+
const tokens = agentSchemaTokens(question);
|
|
6132
|
+
return Array.from(byRelation.values())
|
|
6133
|
+
.map((table) => ({ table, score: scoreAgentSchemaTable(table, tokens) }))
|
|
6134
|
+
.filter((entry) => entry.score > 0)
|
|
6135
|
+
.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
|
|
6136
|
+
.slice(0, 12)
|
|
6137
|
+
.map((entry) => entry.table);
|
|
6138
|
+
}
|
|
6139
|
+
function scoreAgentSchemaTable(table, tokens) {
|
|
6140
|
+
let score = 0;
|
|
6141
|
+
const relationTokens = agentSchemaTokens(`${table.schema ?? ''} ${table.name} ${table.relation}`);
|
|
6142
|
+
for (const token of tokens) {
|
|
6143
|
+
if (relationTokens.has(token))
|
|
6144
|
+
score += 8;
|
|
6145
|
+
}
|
|
6146
|
+
for (const column of table.columns) {
|
|
6147
|
+
const columnTokens = agentSchemaTokens(column.name);
|
|
6148
|
+
for (const token of tokens) {
|
|
6149
|
+
if (columnTokens.has(token))
|
|
6150
|
+
score += 3;
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
if (/(customer|order|revenue|product|location|date|month)/i.test(table.name))
|
|
6154
|
+
score += 1;
|
|
6155
|
+
return score;
|
|
6156
|
+
}
|
|
6157
|
+
function agentSchemaTokens(value) {
|
|
6158
|
+
const tokens = new Set();
|
|
6159
|
+
for (const raw of value.toLowerCase().match(/[a-z0-9_]+/g) ?? []) {
|
|
6160
|
+
for (const part of raw.split('_')) {
|
|
6161
|
+
const normalized = normalizeAgentSchemaToken(part);
|
|
6162
|
+
if (!normalized || normalized.length < 3 || AGENT_SCHEMA_STOPWORDS.has(normalized))
|
|
6163
|
+
continue;
|
|
6164
|
+
tokens.add(normalized);
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
return tokens;
|
|
6168
|
+
}
|
|
6169
|
+
const AGENT_SCHEMA_STOPWORDS = new Set([
|
|
6170
|
+
'all', 'and', 'are', 'can', 'data', 'for', 'from', 'have', 'how', 'many', 'me',
|
|
6171
|
+
'show', 'the', 'this', 'who', 'with', 'value',
|
|
6172
|
+
]);
|
|
6173
|
+
function normalizeAgentSchemaToken(token) {
|
|
6174
|
+
if (token === 'orders')
|
|
6175
|
+
return 'order';
|
|
6176
|
+
if (token === 'customers')
|
|
6177
|
+
return 'customer';
|
|
6178
|
+
if (token === 'products')
|
|
6179
|
+
return 'product';
|
|
6180
|
+
if (token.endsWith('ies') && token.length > 4)
|
|
6181
|
+
return `${token.slice(0, -3)}y`;
|
|
6182
|
+
if (token.endsWith('s') && token.length > 4)
|
|
6183
|
+
return token.slice(0, -1);
|
|
6184
|
+
return token;
|
|
6185
|
+
}
|
|
6186
|
+
function stringFromRecord(record, key) {
|
|
6187
|
+
const value = record[key];
|
|
6188
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
6189
|
+
}
|
|
5485
6190
|
function isMemoryScope(value) {
|
|
5486
6191
|
return value === 'thread'
|
|
5487
6192
|
|| value === 'notebook'
|