@onmartech/metabase-ai-assistant 4.0.0
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/.env.example +38 -0
- package/LICENSE +201 -0
- package/README.md +364 -0
- package/README_MCP.md +279 -0
- package/package.json +99 -0
- package/src/ai/assistant.js +982 -0
- package/src/cli/interactive.js +500 -0
- package/src/database/connection-manager.js +350 -0
- package/src/database/direct-client.js +686 -0
- package/src/index.js +162 -0
- package/src/mcp/handlers/actions.js +213 -0
- package/src/mcp/handlers/ai.js +207 -0
- package/src/mcp/handlers/analytics.js +1647 -0
- package/src/mcp/handlers/cards.js +1544 -0
- package/src/mcp/handlers/collections.js +244 -0
- package/src/mcp/handlers/dashboard.js +207 -0
- package/src/mcp/handlers/dashboard_direct.js +292 -0
- package/src/mcp/handlers/database.js +322 -0
- package/src/mcp/handlers/docs.js +399 -0
- package/src/mcp/handlers/index.js +35 -0
- package/src/mcp/handlers/metadata.js +190 -0
- package/src/mcp/handlers/questions.js +134 -0
- package/src/mcp/handlers/schema.js +1699 -0
- package/src/mcp/handlers/sql.js +559 -0
- package/src/mcp/handlers/users.js +251 -0
- package/src/mcp/job-store.js +199 -0
- package/src/mcp/server.js +428 -0
- package/src/mcp/tool-registry.js +3244 -0
- package/src/mcp/tool-router.js +149 -0
- package/src/metabase/client.js +737 -0
- package/src/metabase/metadata-client.js +1852 -0
- package/src/utils/activity-logger.js +489 -0
- package/src/utils/cache.js +176 -0
- package/src/utils/config.js +131 -0
- package/src/utils/definition-tables.js +938 -0
- package/src/utils/file-operations.js +496 -0
- package/src/utils/logger.js +45 -0
- package/src/utils/parametric-questions.js +627 -0
- package/src/utils/response-optimizer.js +190 -0
- package/src/utils/sql-sanitizer.js +97 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { MetabaseClient } from '../metabase/client.js';
|
|
5
|
+
import { MetabaseAIAssistant } from '../ai/assistant.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
export class InteractiveCLI {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.metabaseClient = new MetabaseClient({
|
|
11
|
+
url: config.metabaseUrl,
|
|
12
|
+
username: config.metabaseUsername,
|
|
13
|
+
password: config.metabasePassword,
|
|
14
|
+
apiKey: config.metabaseApiKey
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
this.assistant = new MetabaseAIAssistant({
|
|
18
|
+
metabaseClient: this.metabaseClient,
|
|
19
|
+
aiProvider: config.aiProvider,
|
|
20
|
+
anthropicApiKey: config.anthropicApiKey,
|
|
21
|
+
openaiApiKey: config.openaiApiKey
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.currentDatabase = null;
|
|
25
|
+
this.currentCollection = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async start() {
|
|
29
|
+
console.log(chalk.cyan.bold('\nš¤ Metabase AI Assistant\n'));
|
|
30
|
+
|
|
31
|
+
// Test connection
|
|
32
|
+
const spinner = ora('Connecting to Metabase...').start();
|
|
33
|
+
try {
|
|
34
|
+
await this.metabaseClient.testConnection();
|
|
35
|
+
spinner.succeed('Connected to Metabase');
|
|
36
|
+
} catch (error) {
|
|
37
|
+
spinner.fail('Failed to connect to Metabase');
|
|
38
|
+
console.error(chalk.red(error.message));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Select database
|
|
43
|
+
await this.selectDatabase();
|
|
44
|
+
|
|
45
|
+
// Main menu loop
|
|
46
|
+
await this.mainMenu();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async selectDatabase() {
|
|
50
|
+
let databases = await this.metabaseClient.getDatabases();
|
|
51
|
+
|
|
52
|
+
// Handle different response formats
|
|
53
|
+
if (!Array.isArray(databases)) {
|
|
54
|
+
if (databases && databases.data && Array.isArray(databases.data)) {
|
|
55
|
+
databases = databases.data;
|
|
56
|
+
} else {
|
|
57
|
+
console.log(chalk.yellow('No databases found or unexpected response format'));
|
|
58
|
+
logger.warn('getDatabases returned non-array:', typeof databases);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (databases.length === 0) {
|
|
64
|
+
console.log(chalk.yellow('No databases found in Metabase'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { database } = await inquirer.prompt([
|
|
69
|
+
{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'database',
|
|
72
|
+
message: 'Select a database:',
|
|
73
|
+
choices: databases.map(db => ({
|
|
74
|
+
name: `${db.name} (${db.engine})`,
|
|
75
|
+
value: db
|
|
76
|
+
}))
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
this.currentDatabase = database;
|
|
81
|
+
console.log(chalk.green(`ā Selected database: ${database.name}`));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async mainMenu() {
|
|
85
|
+
const choices = [
|
|
86
|
+
{ name: 'š Create Model from Description', value: 'create_model' },
|
|
87
|
+
{ name: 'ā Create Question (SQL)', value: 'create_question' },
|
|
88
|
+
{ name: 'š Create Metric', value: 'create_metric' },
|
|
89
|
+
{ name: 'š Create Dashboard', value: 'create_dashboard' },
|
|
90
|
+
{ name: 'š Explore Database Schema', value: 'explore_schema' },
|
|
91
|
+
{ name: 'š Execute SQL Query', value: 'execute_sql' },
|
|
92
|
+
{ name: 'š§ Optimize Existing Query', value: 'optimize_query' },
|
|
93
|
+
{ name: 'š” AI Query Builder', value: 'ai_query_builder' },
|
|
94
|
+
{ name: 'š¦ Batch Operations', value: 'batch_operations' },
|
|
95
|
+
{ name: 'š Switch Database', value: 'switch_database' },
|
|
96
|
+
{ name: 'ā Exit', value: 'exit' }
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const { action } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: 'list',
|
|
102
|
+
name: 'action',
|
|
103
|
+
message: 'What would you like to do?',
|
|
104
|
+
choices
|
|
105
|
+
}
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
switch (action) {
|
|
109
|
+
case 'create_model':
|
|
110
|
+
await this.createModel();
|
|
111
|
+
break;
|
|
112
|
+
case 'create_question':
|
|
113
|
+
await this.createQuestion();
|
|
114
|
+
break;
|
|
115
|
+
case 'create_metric':
|
|
116
|
+
await this.createMetric();
|
|
117
|
+
break;
|
|
118
|
+
case 'create_dashboard':
|
|
119
|
+
await this.createDashboard();
|
|
120
|
+
break;
|
|
121
|
+
case 'explore_schema':
|
|
122
|
+
await this.exploreSchema();
|
|
123
|
+
break;
|
|
124
|
+
case 'execute_sql':
|
|
125
|
+
await this.executeSQL();
|
|
126
|
+
break;
|
|
127
|
+
case 'optimize_query':
|
|
128
|
+
await this.optimizeQuery();
|
|
129
|
+
break;
|
|
130
|
+
case 'ai_query_builder':
|
|
131
|
+
await this.aiQueryBuilder();
|
|
132
|
+
break;
|
|
133
|
+
case 'batch_operations':
|
|
134
|
+
await this.batchOperations();
|
|
135
|
+
break;
|
|
136
|
+
case 'switch_database':
|
|
137
|
+
await this.selectDatabase();
|
|
138
|
+
break;
|
|
139
|
+
case 'exit':
|
|
140
|
+
console.log(chalk.cyan('Goodbye! š'));
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Return to main menu
|
|
145
|
+
await this.mainMenu();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async createModel() {
|
|
149
|
+
const { description } = await inquirer.prompt([
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'description',
|
|
153
|
+
message: 'Describe the model you want to create:',
|
|
154
|
+
validate: input => input.length > 0
|
|
155
|
+
}
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const spinner = ora('Creating model...').start();
|
|
159
|
+
try {
|
|
160
|
+
const model = await this.assistant.createModel(description, this.currentDatabase.id);
|
|
161
|
+
spinner.succeed(`Model created: ${model.name}`);
|
|
162
|
+
console.log(chalk.gray(`ID: ${model.id}`));
|
|
163
|
+
} catch (error) {
|
|
164
|
+
spinner.fail('Failed to create model');
|
|
165
|
+
console.error(chalk.red(error.message));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async createQuestion() {
|
|
170
|
+
const { description, useAI } = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'confirm',
|
|
173
|
+
name: 'useAI',
|
|
174
|
+
message: 'Use AI to generate SQL?',
|
|
175
|
+
default: true
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: 'input',
|
|
179
|
+
name: 'description',
|
|
180
|
+
message: 'Describe the question/query:',
|
|
181
|
+
validate: input => input.length > 0
|
|
182
|
+
}
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const spinner = ora('Creating question...').start();
|
|
186
|
+
try {
|
|
187
|
+
if (useAI) {
|
|
188
|
+
const question = await this.assistant.createQuestion(
|
|
189
|
+
description,
|
|
190
|
+
this.currentDatabase.id,
|
|
191
|
+
this.currentCollection?.id
|
|
192
|
+
);
|
|
193
|
+
spinner.succeed(`Question created: ${question.name}`);
|
|
194
|
+
console.log(chalk.gray(`ID: ${question.id}`));
|
|
195
|
+
} else {
|
|
196
|
+
const { sql } = await inquirer.prompt([
|
|
197
|
+
{
|
|
198
|
+
type: 'editor',
|
|
199
|
+
name: 'sql',
|
|
200
|
+
message: 'Enter SQL query:'
|
|
201
|
+
}
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const question = await this.metabaseClient.createSQLQuestion(
|
|
205
|
+
description,
|
|
206
|
+
description,
|
|
207
|
+
this.currentDatabase.id,
|
|
208
|
+
sql,
|
|
209
|
+
this.currentCollection?.id
|
|
210
|
+
);
|
|
211
|
+
spinner.succeed(`Question created: ${question.name}`);
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
spinner.fail('Failed to create question');
|
|
215
|
+
console.error(chalk.red(error.message));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async createMetric() {
|
|
220
|
+
// Get tables
|
|
221
|
+
const tables = await this.metabaseClient.getDatabaseTables(this.currentDatabase.id);
|
|
222
|
+
|
|
223
|
+
const { table, description } = await inquirer.prompt([
|
|
224
|
+
{
|
|
225
|
+
type: 'list',
|
|
226
|
+
name: 'table',
|
|
227
|
+
message: 'Select table for metric:',
|
|
228
|
+
choices: tables.map(t => ({ name: t.display_name, value: t }))
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: 'input',
|
|
232
|
+
name: 'description',
|
|
233
|
+
message: 'Describe the metric:',
|
|
234
|
+
validate: input => input.length > 0
|
|
235
|
+
}
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
const spinner = ora('Creating metric...').start();
|
|
239
|
+
try {
|
|
240
|
+
const metric = await this.assistant.createMetric(description, table.id);
|
|
241
|
+
spinner.succeed(`Metric created: ${metric.name}`);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
spinner.fail('Failed to create metric');
|
|
244
|
+
console.error(chalk.red(error.message));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async createDashboard() {
|
|
249
|
+
const { name, description, addQuestions } = await inquirer.prompt([
|
|
250
|
+
{
|
|
251
|
+
type: 'input',
|
|
252
|
+
name: 'name',
|
|
253
|
+
message: 'Dashboard name:',
|
|
254
|
+
validate: input => input.length > 0
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: 'input',
|
|
258
|
+
name: 'description',
|
|
259
|
+
message: 'Dashboard description:'
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: 'confirm',
|
|
263
|
+
name: 'addQuestions',
|
|
264
|
+
message: 'Add existing questions to dashboard?',
|
|
265
|
+
default: true
|
|
266
|
+
}
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
let selectedQuestions = [];
|
|
270
|
+
if (addQuestions) {
|
|
271
|
+
const questions = await this.metabaseClient.getQuestions();
|
|
272
|
+
const { selected } = await inquirer.prompt([
|
|
273
|
+
{
|
|
274
|
+
type: 'checkbox',
|
|
275
|
+
name: 'selected',
|
|
276
|
+
message: 'Select questions to add:',
|
|
277
|
+
choices: questions.map(q => ({ name: q.name, value: q }))
|
|
278
|
+
}
|
|
279
|
+
]);
|
|
280
|
+
selectedQuestions = selected;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const spinner = ora('Creating dashboard...').start();
|
|
284
|
+
try {
|
|
285
|
+
const dashboard = await this.assistant.createDashboard(description, selectedQuestions);
|
|
286
|
+
spinner.succeed(`Dashboard created: ${name}`);
|
|
287
|
+
console.log(chalk.gray(`URL: ${this.metabaseClient.baseURL}/dashboard/${dashboard.id}`));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
spinner.fail('Failed to create dashboard');
|
|
290
|
+
console.error(chalk.red(error.message));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async exploreSchema() {
|
|
295
|
+
const schemas = await this.metabaseClient.getDatabaseSchemas(this.currentDatabase.id);
|
|
296
|
+
const tables = await this.metabaseClient.getDatabaseTables(this.currentDatabase.id);
|
|
297
|
+
|
|
298
|
+
console.log(chalk.cyan('\nš Database Schema:\n'));
|
|
299
|
+
console.log(chalk.yellow(`Schemas: ${schemas.join(', ')}`));
|
|
300
|
+
console.log(chalk.yellow(`Tables: ${tables.length}`));
|
|
301
|
+
|
|
302
|
+
const { viewDetails } = await inquirer.prompt([
|
|
303
|
+
{
|
|
304
|
+
type: 'confirm',
|
|
305
|
+
name: 'viewDetails',
|
|
306
|
+
message: 'View table details?',
|
|
307
|
+
default: false
|
|
308
|
+
}
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
if (viewDetails) {
|
|
312
|
+
tables.forEach(table => {
|
|
313
|
+
console.log(chalk.green(`\n${table.display_name} (${table.name})`));
|
|
314
|
+
if (table.fields) {
|
|
315
|
+
table.fields.forEach(field => {
|
|
316
|
+
console.log(` - ${field.display_name}: ${field.base_type}`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async executeSQL() {
|
|
324
|
+
const { sql } = await inquirer.prompt([
|
|
325
|
+
{
|
|
326
|
+
type: 'editor',
|
|
327
|
+
name: 'sql',
|
|
328
|
+
message: 'Enter SQL query to execute:'
|
|
329
|
+
}
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
const spinner = ora('Executing query...').start();
|
|
333
|
+
try {
|
|
334
|
+
const result = await this.metabaseClient.executeNativeQuery(this.currentDatabase.id, sql);
|
|
335
|
+
spinner.succeed('Query executed successfully');
|
|
336
|
+
|
|
337
|
+
// Display results
|
|
338
|
+
if (result.data && result.data.rows) {
|
|
339
|
+
console.table(result.data.rows.slice(0, 10));
|
|
340
|
+
if (result.data.rows.length > 10) {
|
|
341
|
+
console.log(chalk.gray(`... and ${result.data.rows.length - 10} more rows`));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
spinner.fail('Query execution failed');
|
|
346
|
+
console.error(chalk.red(error.message));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async optimizeQuery() {
|
|
351
|
+
const { sql } = await inquirer.prompt([
|
|
352
|
+
{
|
|
353
|
+
type: 'editor',
|
|
354
|
+
name: 'sql',
|
|
355
|
+
message: 'Enter SQL query to optimize:'
|
|
356
|
+
}
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
const spinner = ora('Analyzing query...').start();
|
|
360
|
+
try {
|
|
361
|
+
const optimization = await this.assistant.optimizeQuery(sql);
|
|
362
|
+
spinner.succeed('Query analyzed');
|
|
363
|
+
|
|
364
|
+
console.log(chalk.cyan('\nš§ Optimized Query:\n'));
|
|
365
|
+
console.log(chalk.green(optimization.optimized_sql));
|
|
366
|
+
|
|
367
|
+
console.log(chalk.cyan('\nš Optimizations Applied:'));
|
|
368
|
+
optimization.optimizations.forEach(opt => {
|
|
369
|
+
console.log(chalk.yellow(` ⢠${opt}`));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
console.log(chalk.cyan('\nš Expected Improvements:'));
|
|
373
|
+
console.log(chalk.gray(optimization.improvements));
|
|
374
|
+
} catch (error) {
|
|
375
|
+
spinner.fail('Failed to optimize query');
|
|
376
|
+
console.error(chalk.red(error.message));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async aiQueryBuilder() {
|
|
381
|
+
console.log(chalk.cyan('\nš¤ AI Query Builder\n'));
|
|
382
|
+
console.log(chalk.gray('Describe what data you want in natural language'));
|
|
383
|
+
|
|
384
|
+
const { description } = await inquirer.prompt([
|
|
385
|
+
{
|
|
386
|
+
type: 'input',
|
|
387
|
+
name: 'description',
|
|
388
|
+
message: 'What data do you need?',
|
|
389
|
+
validate: input => input.length > 0
|
|
390
|
+
}
|
|
391
|
+
]);
|
|
392
|
+
|
|
393
|
+
const spinner = ora('Generating SQL...').start();
|
|
394
|
+
try {
|
|
395
|
+
const tables = await this.metabaseClient.getDatabaseTables(this.currentDatabase.id);
|
|
396
|
+
const sql = await this.assistant.generateSQL(description, tables);
|
|
397
|
+
spinner.succeed('SQL generated');
|
|
398
|
+
|
|
399
|
+
console.log(chalk.cyan('\nš Generated SQL:\n'));
|
|
400
|
+
console.log(chalk.green(sql));
|
|
401
|
+
|
|
402
|
+
const { action } = await inquirer.prompt([
|
|
403
|
+
{
|
|
404
|
+
type: 'list',
|
|
405
|
+
name: 'action',
|
|
406
|
+
message: 'What would you like to do?',
|
|
407
|
+
choices: [
|
|
408
|
+
{ name: 'Execute query', value: 'execute' },
|
|
409
|
+
{ name: 'Save as question', value: 'save' },
|
|
410
|
+
{ name: 'Explain query', value: 'explain' },
|
|
411
|
+
{ name: 'Edit query', value: 'edit' },
|
|
412
|
+
{ name: 'Cancel', value: 'cancel' }
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
switch (action) {
|
|
418
|
+
case 'execute':
|
|
419
|
+
await this.executeGeneratedSQL(sql);
|
|
420
|
+
break;
|
|
421
|
+
case 'save':
|
|
422
|
+
await this.saveGeneratedSQL(sql, description);
|
|
423
|
+
break;
|
|
424
|
+
case 'explain':
|
|
425
|
+
await this.explainSQL(sql);
|
|
426
|
+
break;
|
|
427
|
+
case 'edit':
|
|
428
|
+
await this.editAndExecuteSQL(sql);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
spinner.fail('Failed to generate SQL');
|
|
433
|
+
console.error(chalk.red(error.message));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async batchOperations() {
|
|
438
|
+
const { operation } = await inquirer.prompt([
|
|
439
|
+
{
|
|
440
|
+
type: 'list',
|
|
441
|
+
name: 'operation',
|
|
442
|
+
message: 'Select batch operation:',
|
|
443
|
+
choices: [
|
|
444
|
+
{ name: 'Import questions from file', value: 'import_questions' },
|
|
445
|
+
{ name: 'Generate multiple metrics', value: 'generate_metrics' },
|
|
446
|
+
{ name: 'Create dashboard from template', value: 'template_dashboard' },
|
|
447
|
+
{ name: 'Bulk update visualizations', value: 'update_viz' }
|
|
448
|
+
]
|
|
449
|
+
}
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
// Implementation for batch operations
|
|
453
|
+
console.log(chalk.yellow('Batch operation: ' + operation));
|
|
454
|
+
console.log(chalk.gray('This feature is under development'));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Helper methods
|
|
458
|
+
async executeGeneratedSQL(sql) {
|
|
459
|
+
const spinner = ora('Executing query...').start();
|
|
460
|
+
try {
|
|
461
|
+
const result = await this.metabaseClient.executeNativeQuery(this.currentDatabase.id, sql);
|
|
462
|
+
spinner.succeed('Query executed');
|
|
463
|
+
console.table(result.data.rows.slice(0, 10));
|
|
464
|
+
} catch (error) {
|
|
465
|
+
spinner.fail('Execution failed');
|
|
466
|
+
console.error(chalk.red(error.message));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async saveGeneratedSQL(sql, description) {
|
|
471
|
+
const question = await this.metabaseClient.createSQLQuestion(
|
|
472
|
+
description,
|
|
473
|
+
description,
|
|
474
|
+
this.currentDatabase.id,
|
|
475
|
+
sql,
|
|
476
|
+
this.currentCollection?.id
|
|
477
|
+
);
|
|
478
|
+
console.log(chalk.green(`ā Saved as question: ${question.name}`));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async explainSQL(sql) {
|
|
482
|
+
const spinner = ora('Analyzing query...').start();
|
|
483
|
+
const explanation = await this.assistant.explainQuery(sql);
|
|
484
|
+
spinner.succeed('Analysis complete');
|
|
485
|
+
console.log(chalk.cyan('\nš Query Explanation:\n'));
|
|
486
|
+
console.log(chalk.gray(explanation));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async editAndExecuteSQL(sql) {
|
|
490
|
+
const { editedSQL } = await inquirer.prompt([
|
|
491
|
+
{
|
|
492
|
+
type: 'editor',
|
|
493
|
+
name: 'editedSQL',
|
|
494
|
+
message: 'Edit SQL query:',
|
|
495
|
+
default: sql
|
|
496
|
+
}
|
|
497
|
+
]);
|
|
498
|
+
await this.executeGeneratedSQL(editedSQL);
|
|
499
|
+
}
|
|
500
|
+
}
|