@berthojoris/mcp-mysql-server 1.15.0 → 1.16.1

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.
@@ -0,0 +1,713 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.IntelligentQueryTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const config_1 = require("../config/config");
9
+ /**
10
+ * Intelligent Query Assistant
11
+ * Converts natural language to optimized SQL with context-aware query generation
12
+ */
13
+ class IntelligentQueryTools {
14
+ constructor(security) {
15
+ this.db = connection_1.default.getInstance();
16
+ this.security = security;
17
+ }
18
+ /**
19
+ * Validate database access - ensures only the connected database can be accessed
20
+ */
21
+ validateDatabaseAccess(requestedDatabase) {
22
+ const connectedDatabase = config_1.dbConfig.database;
23
+ if (!connectedDatabase) {
24
+ return {
25
+ valid: false,
26
+ database: "",
27
+ error: "No database specified in connection string. Cannot access any database.",
28
+ };
29
+ }
30
+ if (!requestedDatabase) {
31
+ return {
32
+ valid: true,
33
+ database: connectedDatabase,
34
+ };
35
+ }
36
+ if (requestedDatabase !== connectedDatabase) {
37
+ return {
38
+ valid: false,
39
+ database: "",
40
+ error: `Access denied. You can only access the connected database '${connectedDatabase}'. Requested database '${requestedDatabase}' is not allowed.`,
41
+ };
42
+ }
43
+ return {
44
+ valid: true,
45
+ database: connectedDatabase,
46
+ };
47
+ }
48
+ /**
49
+ * Build a natural language query based on intent and context
50
+ * This is the core "Intelligent Query Assistant" feature
51
+ */
52
+ async buildQueryFromIntent(params) {
53
+ try {
54
+ const dbValidation = this.validateDatabaseAccess(params?.database);
55
+ if (!dbValidation.valid) {
56
+ return { status: "error", error: dbValidation.error };
57
+ }
58
+ const { natural_language, context = "analytics", max_complexity = "medium", safety_level = "moderate", } = params;
59
+ const database = dbValidation.database;
60
+ if (!natural_language?.trim()) {
61
+ return {
62
+ status: "error",
63
+ error: "natural_language parameter is required",
64
+ };
65
+ }
66
+ // Step 1: Get database schema context
67
+ const schemaContext = await this.getSchemaContext(database);
68
+ if (!schemaContext.tables.length) {
69
+ return {
70
+ status: "error",
71
+ error: "No tables found in the database. Cannot generate query.",
72
+ };
73
+ }
74
+ // Step 2: Parse natural language intent
75
+ const intentAnalysis = this.analyzeIntent(natural_language, schemaContext);
76
+ // Step 3: Match tables and columns based on intent
77
+ const matchedEntities = this.matchEntitiesToSchema(intentAnalysis, schemaContext);
78
+ // Step 4: Generate SQL based on analysis
79
+ const generatedQuery = this.generateSQL(intentAnalysis, matchedEntities, context, max_complexity, safety_level, database);
80
+ // Step 5: Validate generated query
81
+ const validation = this.security.validateQuery(generatedQuery.sql, false);
82
+ // Step 6: Generate optimization hints
83
+ const optimizationHints = this.generateOptimizationHints(generatedQuery.sql, matchedEntities, schemaContext);
84
+ // Step 7: Generate safety notes
85
+ const safetyNotes = this.generateSafetyNotes(generatedQuery.sql, safety_level, matchedEntities);
86
+ return {
87
+ status: "success",
88
+ data: {
89
+ generated_sql: generatedQuery.sql,
90
+ explanation: generatedQuery.explanation,
91
+ tables_involved: matchedEntities.tables,
92
+ columns_involved: matchedEntities.columns,
93
+ estimated_complexity: this.estimateComplexity(generatedQuery.sql),
94
+ safety_notes: safetyNotes,
95
+ optimization_hints: optimizationHints,
96
+ alternatives: generatedQuery.alternatives,
97
+ },
98
+ };
99
+ }
100
+ catch (error) {
101
+ return {
102
+ status: "error",
103
+ error: error.message,
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * Get database schema context for query generation
109
+ */
110
+ async getSchemaContext(database) {
111
+ // Get tables
112
+ const tables = await this.db.query(`SELECT TABLE_NAME, TABLE_ROWS
113
+ FROM INFORMATION_SCHEMA.TABLES
114
+ WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'
115
+ ORDER BY TABLE_NAME`, [database]);
116
+ if (!tables.length) {
117
+ return { tables: [], relationships: [] };
118
+ }
119
+ // Get columns for all tables
120
+ const tableNames = tables.map((t) => t.TABLE_NAME);
121
+ const placeholders = tableNames.map(() => "?").join(",");
122
+ const columns = await this.db.query(`SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY
123
+ FROM INFORMATION_SCHEMA.COLUMNS
124
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (${placeholders})
125
+ ORDER BY TABLE_NAME, ORDINAL_POSITION`, [database, ...tableNames]);
126
+ // Get foreign key relationships
127
+ const foreignKeys = await this.db.query(`SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
128
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
129
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (${placeholders})
130
+ AND REFERENCED_TABLE_NAME IS NOT NULL`, [database, ...tableNames]);
131
+ // Build relationships map
132
+ const fkMap = new Map();
133
+ foreignKeys.forEach((fk) => {
134
+ fkMap.set(`${fk.TABLE_NAME}.${fk.COLUMN_NAME}`, {
135
+ table: fk.REFERENCED_TABLE_NAME,
136
+ column: fk.REFERENCED_COLUMN_NAME,
137
+ });
138
+ });
139
+ // Build table schema
140
+ const tableSchemas = tables.map((table) => {
141
+ const tableCols = columns.filter((c) => c.TABLE_NAME === table.TABLE_NAME);
142
+ return {
143
+ name: table.TABLE_NAME,
144
+ columns: tableCols.map((col) => {
145
+ const fkRef = fkMap.get(`${col.TABLE_NAME}.${col.COLUMN_NAME}`);
146
+ return {
147
+ name: col.COLUMN_NAME,
148
+ type: col.DATA_TYPE,
149
+ isPrimaryKey: col.COLUMN_KEY === "PRI",
150
+ isForeignKey: !!fkRef,
151
+ referencedTable: fkRef?.table,
152
+ referencedColumn: fkRef?.column,
153
+ };
154
+ }),
155
+ rowCount: parseInt(table.TABLE_ROWS || "0", 10) || 0,
156
+ };
157
+ });
158
+ // Build relationships list
159
+ const relationships = foreignKeys.map((fk) => ({
160
+ fromTable: fk.TABLE_NAME,
161
+ fromColumn: fk.COLUMN_NAME,
162
+ toTable: fk.REFERENCED_TABLE_NAME,
163
+ toColumn: fk.REFERENCED_COLUMN_NAME,
164
+ }));
165
+ return { tables: tableSchemas, relationships };
166
+ }
167
+ /**
168
+ * Analyze natural language intent
169
+ */
170
+ analyzeIntent(naturalLanguage, schemaContext) {
171
+ const text = naturalLanguage.toLowerCase().trim();
172
+ // Detect action type
173
+ let action = "unknown";
174
+ if (/\b(count|how many|number of)\b/i.test(text)) {
175
+ action = "count";
176
+ }
177
+ else if (/\b(total|sum|average|avg|min|max|group by)\b/i.test(text)) {
178
+ action = "aggregate";
179
+ }
180
+ else if (/\b(join|combine|merge|with|and their|along with)\b/i.test(text)) {
181
+ action = "join";
182
+ }
183
+ else if (/\b(show|get|find|list|select|display|retrieve|fetch)\b/i.test(text)) {
184
+ action = "select";
185
+ }
186
+ // Extract keywords (potential table/column references)
187
+ const words = text.split(/\s+/).filter((w) => w.length > 2);
188
+ const keywords = words.filter((w) => !this.isStopWord(w) && /^[a-z_]+$/i.test(w));
189
+ // Detect aggregation functions
190
+ const aggregations = [];
191
+ if (/\btotal\b|\bsum\b/i.test(text))
192
+ aggregations.push("SUM");
193
+ if (/\baverage\b|\bavg\b/i.test(text))
194
+ aggregations.push("AVG");
195
+ if (/\bmin(imum)?\b/i.test(text))
196
+ aggregations.push("MIN");
197
+ if (/\bmax(imum)?\b/i.test(text))
198
+ aggregations.push("MAX");
199
+ if (/\bcount\b|\bhow many\b/i.test(text))
200
+ aggregations.push("COUNT");
201
+ // Detect conditions
202
+ const conditions = [];
203
+ if (/\bwhere\b/i.test(text)) {
204
+ const whereMatch = text.match(/where\s+(.+?)(?:\s+order|\s+limit|\s+group|$)/i);
205
+ if (whereMatch)
206
+ conditions.push(whereMatch[1]);
207
+ }
208
+ // Pattern: "with [column] = [value]" or "[column] is [value]"
209
+ const conditionPatterns = [
210
+ /(\w+)\s*(?:is|=|equals?)\s*['"]?([^'"]+)['"]?/gi,
211
+ /with\s+(\w+)\s+['"]?([^'"]+)['"]?/gi,
212
+ /(\w+)\s+greater\s+than\s+(\d+)/gi,
213
+ /(\w+)\s+less\s+than\s+(\d+)/gi,
214
+ ];
215
+ conditionPatterns.forEach((pattern) => {
216
+ const matches = text.matchAll(pattern);
217
+ for (const match of matches) {
218
+ conditions.push(`${match[1]} = ${match[2]}`);
219
+ }
220
+ });
221
+ // Detect order by
222
+ let orderBy = null;
223
+ const orderPatterns = [
224
+ /order(?:ed)?\s+by\s+(\w+)/i,
225
+ /sort(?:ed)?\s+by\s+(\w+)/i,
226
+ /\b(latest|newest|oldest|highest|lowest|first|last)\b/i,
227
+ ];
228
+ for (const pattern of orderPatterns) {
229
+ const match = text.match(pattern);
230
+ if (match) {
231
+ orderBy = match[1];
232
+ break;
233
+ }
234
+ }
235
+ // Detect limit
236
+ let limit = null;
237
+ const limitPatterns = [
238
+ /(?:top|first|last)\s+(\d+)/i,
239
+ /limit\s+(\d+)/i,
240
+ /(\d+)\s+(?:records?|rows?|items?|entries?)/i,
241
+ ];
242
+ for (const pattern of limitPatterns) {
243
+ const match = text.match(pattern);
244
+ if (match) {
245
+ limit = parseInt(match[1], 10);
246
+ break;
247
+ }
248
+ }
249
+ // Detect group by
250
+ let groupBy = null;
251
+ const groupMatch = text.match(/(?:group|grouped)\s+by\s+(\w+)/i);
252
+ if (groupMatch) {
253
+ groupBy = groupMatch[1];
254
+ }
255
+ else if (/\bper\s+(\w+)\b/i.test(text)) {
256
+ const perMatch = text.match(/\bper\s+(\w+)\b/i);
257
+ if (perMatch)
258
+ groupBy = perMatch[1];
259
+ }
260
+ else if (/\bby\s+(\w+)\b/i.test(text) && action === "aggregate") {
261
+ const byMatch = text.match(/\bby\s+(\w+)\b/i);
262
+ if (byMatch)
263
+ groupBy = byMatch[1];
264
+ }
265
+ return {
266
+ action,
267
+ keywords,
268
+ aggregations,
269
+ conditions,
270
+ orderBy,
271
+ limit,
272
+ groupBy,
273
+ };
274
+ }
275
+ /**
276
+ * Check if a word is a common stop word
277
+ */
278
+ isStopWord(word) {
279
+ const stopWords = new Set([
280
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
281
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
282
+ "should", "may", "might", "must", "shall", "can", "and", "or", "but",
283
+ "if", "then", "else", "when", "where", "which", "who", "whom", "what",
284
+ "how", "why", "all", "each", "every", "both", "few", "more", "most",
285
+ "other", "some", "such", "no", "not", "only", "own", "same", "so",
286
+ "than", "too", "very", "just", "also", "now", "here", "there",
287
+ "show", "get", "find", "list", "select", "display", "retrieve", "fetch",
288
+ "from", "to", "in", "on", "at", "by", "for", "with", "about", "into",
289
+ "through", "during", "before", "after", "above", "below", "up", "down",
290
+ "out", "off", "over", "under", "again", "further", "once", "me", "my",
291
+ ]);
292
+ return stopWords.has(word.toLowerCase());
293
+ }
294
+ /**
295
+ * Match intent entities to actual schema objects
296
+ */
297
+ matchEntitiesToSchema(intent, schemaContext) {
298
+ const matchedTables = [];
299
+ const matchedColumns = [];
300
+ const tableToColumns = new Map();
301
+ const joinPaths = [];
302
+ // Score tables and columns based on keyword matching
303
+ const tableScores = new Map();
304
+ const columnScores = new Map();
305
+ for (const table of schemaContext.tables) {
306
+ const tableName = table.name.toLowerCase();
307
+ let tableScore = 0;
308
+ for (const keyword of intent.keywords) {
309
+ const kw = keyword.toLowerCase();
310
+ // Exact match
311
+ if (tableName === kw) {
312
+ tableScore += 10;
313
+ }
314
+ // Plural/singular match
315
+ else if (tableName === kw + "s" ||
316
+ tableName + "s" === kw ||
317
+ tableName === kw.replace(/ies$/, "y") ||
318
+ tableName.replace(/ies$/, "y") === kw) {
319
+ tableScore += 8;
320
+ }
321
+ // Contains match
322
+ else if (tableName.includes(kw) || kw.includes(tableName)) {
323
+ tableScore += 5;
324
+ }
325
+ // Partial match
326
+ else if (this.similarityScore(tableName, kw) > 0.6) {
327
+ tableScore += 3;
328
+ }
329
+ }
330
+ if (tableScore > 0) {
331
+ tableScores.set(table.name, tableScore);
332
+ }
333
+ // Check columns
334
+ for (const col of table.columns) {
335
+ const colName = col.name.toLowerCase();
336
+ let colScore = 0;
337
+ for (const keyword of intent.keywords) {
338
+ const kw = keyword.toLowerCase();
339
+ if (colName === kw) {
340
+ colScore += 10;
341
+ }
342
+ else if (colName.includes(kw) || kw.includes(colName)) {
343
+ colScore += 5;
344
+ }
345
+ else if (this.similarityScore(colName, kw) > 0.6) {
346
+ colScore += 3;
347
+ }
348
+ }
349
+ // Boost score for aggregation-related columns
350
+ if (intent.aggregations.length > 0) {
351
+ if (col.type.includes("int") ||
352
+ col.type.includes("decimal") ||
353
+ col.type.includes("float") ||
354
+ col.type.includes("double")) {
355
+ colScore += 2;
356
+ }
357
+ }
358
+ if (colScore > 0) {
359
+ const existing = columnScores.get(col.name);
360
+ if (!existing || existing.score < colScore) {
361
+ columnScores.set(col.name, { table: table.name, score: colScore });
362
+ }
363
+ }
364
+ }
365
+ }
366
+ // Select top tables
367
+ const sortedTables = [...tableScores.entries()]
368
+ .sort((a, b) => b[1] - a[1])
369
+ .slice(0, 3);
370
+ for (const [tableName] of sortedTables) {
371
+ matchedTables.push(tableName);
372
+ }
373
+ // If no tables matched, use the largest table as primary
374
+ if (matchedTables.length === 0 && schemaContext.tables.length > 0) {
375
+ const largestTable = schemaContext.tables.reduce((a, b) => a.rowCount > b.rowCount ? a : b);
376
+ matchedTables.push(largestTable.name);
377
+ }
378
+ // Select matched columns
379
+ for (const [colName, { table }] of columnScores) {
380
+ if (matchedTables.includes(table)) {
381
+ matchedColumns.push(colName);
382
+ if (!tableToColumns.has(table)) {
383
+ tableToColumns.set(table, []);
384
+ }
385
+ tableToColumns.get(table).push(colName);
386
+ }
387
+ }
388
+ // Find join paths between matched tables
389
+ if (matchedTables.length > 1) {
390
+ for (const rel of schemaContext.relationships) {
391
+ if (matchedTables.includes(rel.fromTable) &&
392
+ matchedTables.includes(rel.toTable)) {
393
+ joinPaths.push({
394
+ from: rel.fromTable,
395
+ to: rel.toTable,
396
+ on: `${rel.fromTable}.${rel.fromColumn} = ${rel.toTable}.${rel.toColumn}`,
397
+ });
398
+ }
399
+ }
400
+ }
401
+ // Determine primary table
402
+ const primaryTable = matchedTables.length > 0
403
+ ? matchedTables.reduce((a, b) => (tableScores.get(a) || 0) > (tableScores.get(b) || 0) ? a : b)
404
+ : null;
405
+ return {
406
+ tables: matchedTables,
407
+ columns: matchedColumns,
408
+ tableToColumns,
409
+ joinPaths,
410
+ primaryTable,
411
+ };
412
+ }
413
+ /**
414
+ * Calculate similarity score between two strings (simple Jaccard-like)
415
+ */
416
+ similarityScore(a, b) {
417
+ const setA = new Set(a.toLowerCase().split(""));
418
+ const setB = new Set(b.toLowerCase().split(""));
419
+ const intersection = [...setA].filter((x) => setB.has(x)).length;
420
+ const union = new Set([...setA, ...setB]).size;
421
+ return union > 0 ? intersection / union : 0;
422
+ }
423
+ /**
424
+ * Generate SQL based on analysis
425
+ */
426
+ generateSQL(intent, matchedEntities, context, maxComplexity, safetyLevel, database) {
427
+ const alternatives = [];
428
+ let sql = "";
429
+ let explanation = "";
430
+ if (!matchedEntities.primaryTable) {
431
+ return {
432
+ sql: "-- Unable to generate query: no matching tables found",
433
+ explanation: "Could not identify relevant tables from the natural language input.",
434
+ alternatives: [],
435
+ };
436
+ }
437
+ const primaryTable = matchedEntities.primaryTable;
438
+ const columns = matchedEntities.tableToColumns.get(primaryTable) || [];
439
+ // Build SELECT clause
440
+ let selectClause = "";
441
+ if (intent.action === "count") {
442
+ selectClause = "COUNT(*) AS total_count";
443
+ explanation = `Counting records in ${primaryTable}`;
444
+ }
445
+ else if (intent.aggregations.length > 0 && columns.length > 0) {
446
+ const aggCols = columns.slice(0, 3).map((col, i) => {
447
+ const agg = intent.aggregations[i % intent.aggregations.length];
448
+ return `${agg}(\`${col}\`) AS ${agg.toLowerCase()}_${col}`;
449
+ });
450
+ selectClause = aggCols.join(", ");
451
+ explanation = `Aggregating ${intent.aggregations.join(", ")} on ${columns.slice(0, 3).join(", ")}`;
452
+ }
453
+ else if (columns.length > 0) {
454
+ selectClause = columns.map((c) => `\`${c}\``).join(", ");
455
+ explanation = `Selecting columns ${columns.join(", ")} from ${primaryTable}`;
456
+ }
457
+ else {
458
+ selectClause = "*";
459
+ explanation = `Selecting all columns from ${primaryTable}`;
460
+ }
461
+ // Build FROM clause with JOINs
462
+ let fromClause = `\`${database}\`.\`${primaryTable}\``;
463
+ if (matchedEntities.joinPaths.length > 0 && maxComplexity !== "simple") {
464
+ for (const join of matchedEntities.joinPaths) {
465
+ if (join.from === primaryTable) {
466
+ fromClause += `\n LEFT JOIN \`${database}\`.\`${join.to}\` ON ${join.on}`;
467
+ }
468
+ else if (join.to === primaryTable) {
469
+ fromClause += `\n LEFT JOIN \`${database}\`.\`${join.from}\` ON ${join.on}`;
470
+ }
471
+ }
472
+ explanation += ` with joins to ${matchedEntities.tables.filter((t) => t !== primaryTable).join(", ")}`;
473
+ }
474
+ // Build WHERE clause
475
+ let whereClause = "";
476
+ if (intent.conditions.length > 0) {
477
+ // Parse simple conditions
478
+ const parsedConditions = intent.conditions.map((cond) => {
479
+ const parts = cond.match(/(\w+)\s*(?:=|is|equals?)\s*(.+)/i);
480
+ if (parts) {
481
+ const [, col, val] = parts;
482
+ return `\`${col}\` = '${val.trim()}'`;
483
+ }
484
+ return null;
485
+ }).filter(Boolean);
486
+ if (parsedConditions.length > 0) {
487
+ whereClause = `\nWHERE ${parsedConditions.join(" AND ")}`;
488
+ }
489
+ }
490
+ // Build GROUP BY clause
491
+ let groupByClause = "";
492
+ if (intent.groupBy) {
493
+ groupByClause = `\nGROUP BY \`${intent.groupBy}\``;
494
+ explanation += `, grouped by ${intent.groupBy}`;
495
+ }
496
+ // Build ORDER BY clause
497
+ let orderByClause = "";
498
+ if (intent.orderBy) {
499
+ const direction = /\b(latest|newest|highest|last)\b/i.test(intent.orderBy)
500
+ ? "DESC"
501
+ : "ASC";
502
+ const orderCol = intent.orderBy.match(/^(latest|newest|oldest|highest|lowest|first|last)$/i)
503
+ ? columns[0] || "id"
504
+ : intent.orderBy;
505
+ orderByClause = `\nORDER BY \`${orderCol}\` ${direction}`;
506
+ }
507
+ // Build LIMIT clause
508
+ let limitClause = "";
509
+ if (intent.limit) {
510
+ limitClause = `\nLIMIT ${intent.limit}`;
511
+ }
512
+ else if (safetyLevel !== "permissive" && context !== "data_entry") {
513
+ // Add safety limit for non-permissive modes
514
+ limitClause = "\nLIMIT 100";
515
+ explanation += " (limited to 100 rows for safety)";
516
+ }
517
+ // Assemble final SQL
518
+ sql = `SELECT ${selectClause}\nFROM ${fromClause}${whereClause}${groupByClause}${orderByClause}${limitClause}`;
519
+ // Generate alternatives
520
+ if (intent.action !== "count") {
521
+ alternatives.push(`SELECT COUNT(*) FROM \`${database}\`.\`${primaryTable}\`${whereClause}`);
522
+ }
523
+ if (!groupByClause && columns.length > 0) {
524
+ alternatives.push(`SELECT ${columns[0]}, COUNT(*) AS count FROM \`${database}\`.\`${primaryTable}\`${whereClause} GROUP BY \`${columns[0]}\` ORDER BY count DESC LIMIT 10`);
525
+ }
526
+ return { sql, explanation, alternatives };
527
+ }
528
+ /**
529
+ * Estimate query complexity
530
+ */
531
+ estimateComplexity(sql) {
532
+ const lowerSql = sql.toLowerCase();
533
+ let score = 0;
534
+ if (lowerSql.includes("join"))
535
+ score += 2;
536
+ if (lowerSql.includes("group by"))
537
+ score += 1;
538
+ if (lowerSql.includes("having"))
539
+ score += 1;
540
+ if (lowerSql.includes("subquery") || (lowerSql.match(/select/g) || []).length > 1)
541
+ score += 3;
542
+ if (lowerSql.includes("union"))
543
+ score += 2;
544
+ if ((lowerSql.match(/and|or/g) || []).length > 3)
545
+ score += 1;
546
+ if (score >= 5)
547
+ return "HIGH";
548
+ if (score >= 2)
549
+ return "MEDIUM";
550
+ return "LOW";
551
+ }
552
+ /**
553
+ * Generate optimization hints
554
+ */
555
+ generateOptimizationHints(sql, matchedEntities, schemaContext) {
556
+ const hints = [];
557
+ // Check for SELECT *
558
+ if (sql.includes("SELECT *")) {
559
+ hints.push("Consider selecting specific columns instead of '*' for better performance.");
560
+ }
561
+ // Check for missing LIMIT
562
+ if (!sql.toLowerCase().includes("limit")) {
563
+ hints.push("Consider adding a LIMIT clause to prevent fetching too many rows.");
564
+ }
565
+ // Check for JOINs without proper indexes hint
566
+ if (sql.toLowerCase().includes("join")) {
567
+ hints.push("Ensure JOIN columns are indexed for optimal performance.");
568
+ }
569
+ // Check for large tables
570
+ const largeTables = schemaContext.tables.filter((t) => matchedEntities.tables.includes(t.name) && t.columns.length > 20);
571
+ if (largeTables.length > 0) {
572
+ hints.push(`Tables ${largeTables.map((t) => t.name).join(", ")} have many columns. Select only needed columns.`);
573
+ }
574
+ // Suggest using EXPLAIN
575
+ hints.push("Use EXPLAIN to analyze query execution plan before running on large datasets.");
576
+ return hints;
577
+ }
578
+ /**
579
+ * Generate safety notes
580
+ */
581
+ generateSafetyNotes(sql, safetyLevel, matchedEntities) {
582
+ const notes = [];
583
+ if (!sql.toLowerCase().includes("limit")) {
584
+ notes.push("Query has no LIMIT - may return large result sets.");
585
+ }
586
+ if (matchedEntities.tables.length > 2) {
587
+ notes.push("Query involves multiple tables - verify JOIN conditions are correct.");
588
+ }
589
+ if (safetyLevel === "strict") {
590
+ notes.push("Running in strict safety mode - only SELECT queries are allowed.");
591
+ }
592
+ // Check for potentially sensitive column names
593
+ const sensitivePatterns = ["password", "secret", "token", "ssn", "credit"];
594
+ for (const col of matchedEntities.columns) {
595
+ if (sensitivePatterns.some((p) => col.toLowerCase().includes(p))) {
596
+ notes.push(`Column '${col}' may contain sensitive data. Ensure proper access controls.`);
597
+ }
598
+ }
599
+ return notes;
600
+ }
601
+ /**
602
+ * Suggest query improvements
603
+ */
604
+ async suggestQueryImprovements(params) {
605
+ try {
606
+ const dbValidation = this.validateDatabaseAccess(params?.database);
607
+ if (!dbValidation.valid) {
608
+ return { status: "error", error: dbValidation.error };
609
+ }
610
+ const { query, optimization_goal = "speed", } = params;
611
+ if (!query?.trim()) {
612
+ return { status: "error", error: "query parameter is required" };
613
+ }
614
+ const suggestions = [];
615
+ const lowerQuery = query.toLowerCase();
616
+ // Check for SELECT *
617
+ if (lowerQuery.includes("select *")) {
618
+ suggestions.push({
619
+ type: "COLUMN_SELECTION",
620
+ description: "Replace SELECT * with specific column names for better performance",
621
+ improved_query: query.replace(/select\s+\*/i, "SELECT /* specify columns here */"),
622
+ });
623
+ }
624
+ // Check for missing WHERE clause with DELETE/UPDATE
625
+ if ((lowerQuery.includes("delete") || lowerQuery.includes("update")) &&
626
+ !lowerQuery.includes("where")) {
627
+ suggestions.push({
628
+ type: "SAFETY",
629
+ description: "DELETE/UPDATE without WHERE clause will affect all rows - add conditions",
630
+ });
631
+ }
632
+ // Check for inefficient LIKE patterns
633
+ if (lowerQuery.match(/like\s+['"]%/)) {
634
+ suggestions.push({
635
+ type: "INDEX_USAGE",
636
+ description: "Leading wildcard in LIKE pattern prevents index usage. Consider FULLTEXT search.",
637
+ });
638
+ }
639
+ // Check for functions on indexed columns
640
+ if (lowerQuery.match(/where\s+\w+\s*\([^)]+\)\s*=/)) {
641
+ suggestions.push({
642
+ type: "INDEX_USAGE",
643
+ description: "Using functions on columns in WHERE clause prevents index usage. Move function to the right side.",
644
+ });
645
+ }
646
+ // Check for ORDER BY without LIMIT
647
+ if (lowerQuery.includes("order by") && !lowerQuery.includes("limit")) {
648
+ suggestions.push({
649
+ type: "PERFORMANCE",
650
+ description: "ORDER BY without LIMIT may be slow on large datasets. Consider adding LIMIT.",
651
+ });
652
+ }
653
+ // Check for SELECT DISTINCT on many columns
654
+ if (lowerQuery.match(/select\s+distinct.*,.*,/)) {
655
+ suggestions.push({
656
+ type: "PERFORMANCE",
657
+ description: "SELECT DISTINCT on multiple columns can be slow. Consider using GROUP BY instead.",
658
+ });
659
+ }
660
+ // Memory optimization suggestions
661
+ if (optimization_goal === "memory") {
662
+ if (!lowerQuery.includes("limit")) {
663
+ suggestions.push({
664
+ type: "MEMORY",
665
+ description: "Add LIMIT clause to reduce memory usage for large result sets.",
666
+ });
667
+ }
668
+ if (lowerQuery.includes("order by")) {
669
+ suggestions.push({
670
+ type: "MEMORY",
671
+ description: "ORDER BY requires memory for sorting. Consider indexing the ORDER BY column.",
672
+ });
673
+ }
674
+ }
675
+ // Readability suggestions
676
+ if (optimization_goal === "readability") {
677
+ if (!lowerQuery.includes("\n")) {
678
+ suggestions.push({
679
+ type: "READABILITY",
680
+ description: "Consider formatting query with line breaks for better readability.",
681
+ });
682
+ }
683
+ if (lowerQuery.match(/\bt\d+\b|\btbl\d+\b/)) {
684
+ suggestions.push({
685
+ type: "READABILITY",
686
+ description: "Use meaningful table aliases instead of t1, t2, tbl1, etc.",
687
+ });
688
+ }
689
+ }
690
+ if (suggestions.length === 0) {
691
+ suggestions.push({
692
+ type: "GENERAL",
693
+ description: "Query appears well-formed. Use EXPLAIN for detailed analysis.",
694
+ });
695
+ }
696
+ return {
697
+ status: "success",
698
+ data: {
699
+ original_query: query,
700
+ suggestions,
701
+ estimated_improvement: suggestions.length > 2 ? "SIGNIFICANT" : suggestions.length > 0 ? "MODERATE" : "MINIMAL",
702
+ },
703
+ };
704
+ }
705
+ catch (error) {
706
+ return {
707
+ status: "error",
708
+ error: error.message,
709
+ };
710
+ }
711
+ }
712
+ }
713
+ exports.IntelligentQueryTools = IntelligentQueryTools;