@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.
- package/CHANGELOG.md +40 -0
- package/DOCUMENTATIONS.md +292 -5
- package/README.md +14 -13
- package/dist/config/featureConfig.d.ts +2 -1
- package/dist/config/featureConfig.js +20 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +60 -0
- package/dist/mcp-server.js +311 -4
- package/dist/tools/ddlTools.js +1 -1
- package/dist/tools/documentationGeneratorTools.d.ts +145 -0
- package/dist/tools/documentationGeneratorTools.js +820 -0
- package/dist/tools/intelligentQueryTools.d.ts +94 -0
- package/dist/tools/intelligentQueryTools.js +713 -0
- package/dist/tools/smartDiscoveryTools.d.ts +163 -0
- package/dist/tools/smartDiscoveryTools.js +750 -0
- package/package.json +1 -1
|
@@ -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;
|