@berthojoris/mcp-mysql-server 1.4.14 → 1.5.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/CHANGELOG.md +13 -0
- package/DOCUMENTATIONS.md +200 -18
- package/README.md +30 -29
- package/dist/cache/queryCache.d.ts +126 -0
- package/dist/cache/queryCache.js +337 -0
- package/dist/config/featureConfig.js +82 -71
- package/dist/db/connection.d.ts +21 -2
- package/dist/db/connection.js +73 -7
- package/dist/db/queryLogger.d.ts +3 -2
- package/dist/db/queryLogger.js +64 -43
- package/dist/index.d.ts +76 -3
- package/dist/index.js +161 -70
- package/dist/mcp-server.js +166 -5
- package/dist/optimization/queryOptimizer.d.ts +125 -0
- package/dist/optimization/queryOptimizer.js +509 -0
- package/dist/tools/queryTools.d.ts +14 -1
- package/dist/tools/queryTools.js +27 -3
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL Query Optimization Hints
|
|
3
|
+
*
|
|
4
|
+
* Supports MySQL 8.0+ optimizer hints and provides query analysis
|
|
5
|
+
* for performance optimization suggestions.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Available optimizer hint types for MySQL 8.0+
|
|
9
|
+
*/
|
|
10
|
+
export type OptimizerHintType = "JOIN_FIXED_ORDER" | "JOIN_ORDER" | "JOIN_PREFIX" | "JOIN_SUFFIX" | "BKA" | "NO_BKA" | "BNL" | "NO_BNL" | "HASH_JOIN" | "NO_HASH_JOIN" | "MERGE" | "NO_MERGE" | "INDEX" | "NO_INDEX" | "INDEX_MERGE" | "NO_INDEX_MERGE" | "MRR" | "NO_MRR" | "NO_ICP" | "NO_RANGE_OPTIMIZATION" | "SKIP_SCAN" | "NO_SKIP_SCAN" | "SEMIJOIN" | "NO_SEMIJOIN" | "SUBQUERY" | "SQL_NO_CACHE" | "SQL_CACHE" | "MAX_EXECUTION_TIME" | "RESOURCE_GROUP" | "SET_VAR";
|
|
11
|
+
/**
|
|
12
|
+
* Optimizer hint configuration
|
|
13
|
+
*/
|
|
14
|
+
export interface OptimizerHint {
|
|
15
|
+
type: OptimizerHintType;
|
|
16
|
+
table?: string;
|
|
17
|
+
index?: string | string[];
|
|
18
|
+
value?: string | number;
|
|
19
|
+
tables?: string[];
|
|
20
|
+
strategy?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Query optimization hints configuration
|
|
24
|
+
*/
|
|
25
|
+
export interface QueryHints {
|
|
26
|
+
hints?: OptimizerHint[];
|
|
27
|
+
forceIndex?: string | string[];
|
|
28
|
+
ignoreIndex?: string | string[];
|
|
29
|
+
useIndex?: string | string[];
|
|
30
|
+
maxExecutionTime?: number;
|
|
31
|
+
straightJoin?: boolean;
|
|
32
|
+
noCache?: boolean;
|
|
33
|
+
highPriority?: boolean;
|
|
34
|
+
sqlBigResult?: boolean;
|
|
35
|
+
sqlSmallResult?: boolean;
|
|
36
|
+
sqlBufferResult?: boolean;
|
|
37
|
+
sqlCalcFoundRows?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Query analysis result
|
|
41
|
+
*/
|
|
42
|
+
export interface QueryAnalysis {
|
|
43
|
+
originalQuery: string;
|
|
44
|
+
queryType: "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
|
|
45
|
+
tables: string[];
|
|
46
|
+
hasJoins: boolean;
|
|
47
|
+
hasSubqueries: boolean;
|
|
48
|
+
hasGroupBy: boolean;
|
|
49
|
+
hasOrderBy: boolean;
|
|
50
|
+
hasLimit: boolean;
|
|
51
|
+
estimatedComplexity: "LOW" | "MEDIUM" | "HIGH";
|
|
52
|
+
suggestions: OptimizationSuggestion[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Optimization suggestion
|
|
56
|
+
*/
|
|
57
|
+
export interface OptimizationSuggestion {
|
|
58
|
+
type: "INDEX" | "HINT" | "REWRITE" | "STRUCTURE";
|
|
59
|
+
priority: "LOW" | "MEDIUM" | "HIGH";
|
|
60
|
+
description: string;
|
|
61
|
+
suggestedAction: string;
|
|
62
|
+
hint?: OptimizerHint;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Query Optimizer class
|
|
66
|
+
* Provides MySQL query optimization hints and analysis
|
|
67
|
+
*/
|
|
68
|
+
export declare class QueryOptimizer {
|
|
69
|
+
private static instance;
|
|
70
|
+
private constructor();
|
|
71
|
+
/**
|
|
72
|
+
* Get singleton instance
|
|
73
|
+
*/
|
|
74
|
+
static getInstance(): QueryOptimizer;
|
|
75
|
+
/**
|
|
76
|
+
* Escape special regex characters in a string
|
|
77
|
+
*/
|
|
78
|
+
private escapeRegex;
|
|
79
|
+
/**
|
|
80
|
+
* Sanitize identifier (table name, index name) to prevent injection
|
|
81
|
+
* Only allows alphanumeric characters, underscores, and dots (for schema.table)
|
|
82
|
+
*/
|
|
83
|
+
private sanitizeIdentifier;
|
|
84
|
+
/**
|
|
85
|
+
* Validate that an identifier is safe to use
|
|
86
|
+
*/
|
|
87
|
+
private isValidIdentifier;
|
|
88
|
+
/**
|
|
89
|
+
* Apply optimizer hints to a SELECT query
|
|
90
|
+
*/
|
|
91
|
+
applyHints(query: string, hints: QueryHints): string;
|
|
92
|
+
/**
|
|
93
|
+
* Build the optimizer hint block string
|
|
94
|
+
*/
|
|
95
|
+
private buildHintBlock;
|
|
96
|
+
/**
|
|
97
|
+
* Format a single optimizer hint
|
|
98
|
+
*/
|
|
99
|
+
private formatHint;
|
|
100
|
+
/**
|
|
101
|
+
* Apply traditional USE INDEX / FORCE INDEX / IGNORE INDEX syntax
|
|
102
|
+
*/
|
|
103
|
+
private applyIndexHintsTraditional;
|
|
104
|
+
/**
|
|
105
|
+
* Analyze a query and provide optimization suggestions
|
|
106
|
+
*/
|
|
107
|
+
analyzeQuery(query: string): QueryAnalysis;
|
|
108
|
+
/**
|
|
109
|
+
* Extract table names from a query
|
|
110
|
+
*/
|
|
111
|
+
private extractTables;
|
|
112
|
+
/**
|
|
113
|
+
* Check if a word is a SQL keyword
|
|
114
|
+
*/
|
|
115
|
+
private isKeyword;
|
|
116
|
+
/**
|
|
117
|
+
* Generate optimization suggestions based on query analysis
|
|
118
|
+
*/
|
|
119
|
+
private generateSuggestions;
|
|
120
|
+
/**
|
|
121
|
+
* Get suggested hints for a specific optimization goal
|
|
122
|
+
*/
|
|
123
|
+
getSuggestedHints(goal: "SPEED" | "MEMORY" | "STABILITY"): QueryHints;
|
|
124
|
+
}
|
|
125
|
+
export default QueryOptimizer;
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MySQL Query Optimization Hints
|
|
4
|
+
*
|
|
5
|
+
* Supports MySQL 8.0+ optimizer hints and provides query analysis
|
|
6
|
+
* for performance optimization suggestions.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.QueryOptimizer = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Query Optimizer class
|
|
12
|
+
* Provides MySQL query optimization hints and analysis
|
|
13
|
+
*/
|
|
14
|
+
class QueryOptimizer {
|
|
15
|
+
constructor() { }
|
|
16
|
+
/**
|
|
17
|
+
* Get singleton instance
|
|
18
|
+
*/
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!QueryOptimizer.instance) {
|
|
21
|
+
QueryOptimizer.instance = new QueryOptimizer();
|
|
22
|
+
}
|
|
23
|
+
return QueryOptimizer.instance;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Escape special regex characters in a string
|
|
27
|
+
*/
|
|
28
|
+
escapeRegex(str) {
|
|
29
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize identifier (table name, index name) to prevent injection
|
|
33
|
+
* Only allows alphanumeric characters, underscores, and dots (for schema.table)
|
|
34
|
+
*/
|
|
35
|
+
sanitizeIdentifier(identifier) {
|
|
36
|
+
// Remove any characters that aren't alphanumeric, underscore, or dot
|
|
37
|
+
return identifier.replace(/[^a-zA-Z0-9_\.]/g, "");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate that an identifier is safe to use
|
|
41
|
+
*/
|
|
42
|
+
isValidIdentifier(identifier) {
|
|
43
|
+
// Must start with letter or underscore, contain only valid chars
|
|
44
|
+
// Max length 64 (MySQL limit)
|
|
45
|
+
return /^[a-zA-Z_][a-zA-Z0-9_\.]{0,63}$/.test(identifier);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Apply optimizer hints to a SELECT query
|
|
49
|
+
*/
|
|
50
|
+
applyHints(query, hints) {
|
|
51
|
+
const normalizedQuery = query.trim();
|
|
52
|
+
const upperQuery = normalizedQuery.toUpperCase();
|
|
53
|
+
// Only apply hints to SELECT queries
|
|
54
|
+
if (!upperQuery.startsWith("SELECT")) {
|
|
55
|
+
return normalizedQuery;
|
|
56
|
+
}
|
|
57
|
+
let hintBlock = this.buildHintBlock(hints);
|
|
58
|
+
let modifiedQuery = normalizedQuery;
|
|
59
|
+
// Handle STRAIGHT_JOIN
|
|
60
|
+
if (hints.straightJoin) {
|
|
61
|
+
modifiedQuery = modifiedQuery.replace(/^SELECT/i, "SELECT STRAIGHT_JOIN");
|
|
62
|
+
}
|
|
63
|
+
// Handle SQL modifiers (HIGH_PRIORITY, SQL_BIG_RESULT, etc.)
|
|
64
|
+
const sqlModifiers = [];
|
|
65
|
+
if (hints.highPriority)
|
|
66
|
+
sqlModifiers.push("HIGH_PRIORITY");
|
|
67
|
+
if (hints.sqlBigResult)
|
|
68
|
+
sqlModifiers.push("SQL_BIG_RESULT");
|
|
69
|
+
if (hints.sqlSmallResult)
|
|
70
|
+
sqlModifiers.push("SQL_SMALL_RESULT");
|
|
71
|
+
if (hints.sqlBufferResult)
|
|
72
|
+
sqlModifiers.push("SQL_BUFFER_RESULT");
|
|
73
|
+
if (hints.sqlCalcFoundRows)
|
|
74
|
+
sqlModifiers.push("SQL_CALC_FOUND_ROWS");
|
|
75
|
+
if (hints.noCache)
|
|
76
|
+
sqlModifiers.push("SQL_NO_CACHE");
|
|
77
|
+
if (sqlModifiers.length > 0) {
|
|
78
|
+
const modifiersStr = sqlModifiers.join(" ");
|
|
79
|
+
modifiedQuery = modifiedQuery.replace(/^SELECT(\s+STRAIGHT_JOIN)?/i, `SELECT$1 ${modifiersStr}`);
|
|
80
|
+
}
|
|
81
|
+
// Insert hint block after SELECT keyword
|
|
82
|
+
if (hintBlock) {
|
|
83
|
+
// Find the position after SELECT (and any modifiers)
|
|
84
|
+
const selectMatch = modifiedQuery.match(/^SELECT(\s+STRAIGHT_JOIN)?(\s+(?:HIGH_PRIORITY|SQL_BIG_RESULT|SQL_SMALL_RESULT|SQL_BUFFER_RESULT|SQL_CALC_FOUND_ROWS|SQL_NO_CACHE)\s*)*/i);
|
|
85
|
+
if (selectMatch) {
|
|
86
|
+
const insertPos = selectMatch[0].length;
|
|
87
|
+
modifiedQuery =
|
|
88
|
+
modifiedQuery.slice(0, insertPos) +
|
|
89
|
+
` /*+ ${hintBlock} */ ` +
|
|
90
|
+
modifiedQuery.slice(insertPos);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Handle USE INDEX / FORCE INDEX / IGNORE INDEX (traditional syntax)
|
|
94
|
+
modifiedQuery = this.applyIndexHintsTraditional(modifiedQuery, hints);
|
|
95
|
+
return modifiedQuery.replace(/\s+/g, " ").trim();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the optimizer hint block string
|
|
99
|
+
*/
|
|
100
|
+
buildHintBlock(hints) {
|
|
101
|
+
const hintParts = [];
|
|
102
|
+
// Process explicit hints array
|
|
103
|
+
if (hints.hints && hints.hints.length > 0) {
|
|
104
|
+
for (const hint of hints.hints) {
|
|
105
|
+
const hintStr = this.formatHint(hint);
|
|
106
|
+
if (hintStr) {
|
|
107
|
+
hintParts.push(hintStr);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Process shorthand hints
|
|
112
|
+
if (hints.maxExecutionTime !== undefined) {
|
|
113
|
+
// Validate maxExecutionTime is a positive integer
|
|
114
|
+
const maxTime = Math.floor(Math.abs(Number(hints.maxExecutionTime)));
|
|
115
|
+
if (maxTime > 0 && maxTime <= 31536000000) {
|
|
116
|
+
// Max 1 year in ms
|
|
117
|
+
hintParts.push(`MAX_EXECUTION_TIME(${maxTime})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (hints.forceIndex) {
|
|
121
|
+
const indexes = Array.isArray(hints.forceIndex)
|
|
122
|
+
? hints.forceIndex
|
|
123
|
+
: [hints.forceIndex];
|
|
124
|
+
const sanitizedIndexes = indexes
|
|
125
|
+
.map((idx) => this.sanitizeIdentifier(idx))
|
|
126
|
+
.filter((idx) => this.isValidIdentifier(idx));
|
|
127
|
+
if (sanitizedIndexes.length > 0) {
|
|
128
|
+
hintParts.push(`INDEX(${sanitizedIndexes.join(", ")})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (hints.ignoreIndex) {
|
|
132
|
+
const indexes = Array.isArray(hints.ignoreIndex)
|
|
133
|
+
? hints.ignoreIndex
|
|
134
|
+
: [hints.ignoreIndex];
|
|
135
|
+
const sanitizedIndexes = indexes
|
|
136
|
+
.map((idx) => this.sanitizeIdentifier(idx))
|
|
137
|
+
.filter((idx) => this.isValidIdentifier(idx));
|
|
138
|
+
if (sanitizedIndexes.length > 0) {
|
|
139
|
+
hintParts.push(`NO_INDEX(${sanitizedIndexes.join(", ")})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return hintParts.join(" ");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Format a single optimizer hint
|
|
146
|
+
*/
|
|
147
|
+
formatHint(hint) {
|
|
148
|
+
switch (hint.type) {
|
|
149
|
+
// Join-Order Hints
|
|
150
|
+
case "JOIN_FIXED_ORDER":
|
|
151
|
+
return "JOIN_FIXED_ORDER()";
|
|
152
|
+
case "JOIN_ORDER":
|
|
153
|
+
case "JOIN_PREFIX":
|
|
154
|
+
case "JOIN_SUFFIX":
|
|
155
|
+
if (hint.tables && hint.tables.length > 0) {
|
|
156
|
+
return `${hint.type}(${hint.tables.join(", ")})`;
|
|
157
|
+
}
|
|
158
|
+
return "";
|
|
159
|
+
// Table-Level Hints
|
|
160
|
+
case "BKA":
|
|
161
|
+
case "NO_BKA":
|
|
162
|
+
case "BNL":
|
|
163
|
+
case "NO_BNL":
|
|
164
|
+
case "HASH_JOIN":
|
|
165
|
+
case "NO_HASH_JOIN":
|
|
166
|
+
case "MERGE":
|
|
167
|
+
case "NO_MERGE":
|
|
168
|
+
if (hint.table) {
|
|
169
|
+
return `${hint.type}(${hint.table})`;
|
|
170
|
+
}
|
|
171
|
+
return `${hint.type}()`;
|
|
172
|
+
// Index-Level Hints
|
|
173
|
+
case "INDEX":
|
|
174
|
+
case "NO_INDEX":
|
|
175
|
+
case "INDEX_MERGE":
|
|
176
|
+
case "NO_INDEX_MERGE":
|
|
177
|
+
case "MRR":
|
|
178
|
+
case "NO_MRR":
|
|
179
|
+
case "NO_ICP":
|
|
180
|
+
case "NO_RANGE_OPTIMIZATION":
|
|
181
|
+
case "SKIP_SCAN":
|
|
182
|
+
case "NO_SKIP_SCAN":
|
|
183
|
+
if (hint.table && hint.index) {
|
|
184
|
+
const indexes = Array.isArray(hint.index)
|
|
185
|
+
? hint.index.join(", ")
|
|
186
|
+
: hint.index;
|
|
187
|
+
return `${hint.type}(${hint.table} ${indexes})`;
|
|
188
|
+
}
|
|
189
|
+
else if (hint.table) {
|
|
190
|
+
return `${hint.type}(${hint.table})`;
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
// Subquery Hints
|
|
194
|
+
case "SEMIJOIN":
|
|
195
|
+
case "NO_SEMIJOIN":
|
|
196
|
+
if (hint.strategy) {
|
|
197
|
+
return `${hint.type}(${hint.strategy})`;
|
|
198
|
+
}
|
|
199
|
+
return `${hint.type}()`;
|
|
200
|
+
case "SUBQUERY":
|
|
201
|
+
if (hint.strategy) {
|
|
202
|
+
return `SUBQUERY(${hint.strategy})`;
|
|
203
|
+
}
|
|
204
|
+
return "";
|
|
205
|
+
// Miscellaneous
|
|
206
|
+
case "MAX_EXECUTION_TIME":
|
|
207
|
+
if (hint.value !== undefined) {
|
|
208
|
+
return `MAX_EXECUTION_TIME(${hint.value})`;
|
|
209
|
+
}
|
|
210
|
+
return "";
|
|
211
|
+
case "RESOURCE_GROUP":
|
|
212
|
+
if (hint.value) {
|
|
213
|
+
return `RESOURCE_GROUP(${hint.value})`;
|
|
214
|
+
}
|
|
215
|
+
return "";
|
|
216
|
+
case "SET_VAR":
|
|
217
|
+
if (hint.value) {
|
|
218
|
+
return `SET_VAR(${hint.value})`;
|
|
219
|
+
}
|
|
220
|
+
return "";
|
|
221
|
+
// Cache hints (legacy)
|
|
222
|
+
case "SQL_NO_CACHE":
|
|
223
|
+
case "SQL_CACHE":
|
|
224
|
+
return ""; // These are handled as SQL modifiers, not hint block
|
|
225
|
+
default:
|
|
226
|
+
return "";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Apply traditional USE INDEX / FORCE INDEX / IGNORE INDEX syntax
|
|
231
|
+
*/
|
|
232
|
+
applyIndexHintsTraditional(query, hints) {
|
|
233
|
+
if (!hints.useIndex) {
|
|
234
|
+
return query;
|
|
235
|
+
}
|
|
236
|
+
// Find table references and add USE INDEX after them
|
|
237
|
+
const indexes = Array.isArray(hints.useIndex)
|
|
238
|
+
? hints.useIndex
|
|
239
|
+
: [hints.useIndex];
|
|
240
|
+
const useIndexClause = `USE INDEX (${indexes.join(", ")})`;
|
|
241
|
+
// Simple approach: add after FROM clause table
|
|
242
|
+
// This is a basic implementation - complex queries may need more sophisticated parsing
|
|
243
|
+
const fromMatch = query.match(/FROM\s+[\`"']?(\w+)[\`"']?(\s+(?:AS\s+)?[\`"']?\w+[\`"']?)?/i);
|
|
244
|
+
if (fromMatch) {
|
|
245
|
+
const fullMatch = fromMatch[0];
|
|
246
|
+
const insertPos = query.indexOf(fullMatch) + fullMatch.length;
|
|
247
|
+
return (query.slice(0, insertPos) +
|
|
248
|
+
" " +
|
|
249
|
+
useIndexClause +
|
|
250
|
+
query.slice(insertPos));
|
|
251
|
+
}
|
|
252
|
+
return query;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Analyze a query and provide optimization suggestions
|
|
256
|
+
*/
|
|
257
|
+
analyzeQuery(query) {
|
|
258
|
+
const normalizedQuery = query.trim();
|
|
259
|
+
const upperQuery = normalizedQuery.toUpperCase();
|
|
260
|
+
// Determine query type
|
|
261
|
+
let queryType = "OTHER";
|
|
262
|
+
if (upperQuery.startsWith("SELECT"))
|
|
263
|
+
queryType = "SELECT";
|
|
264
|
+
else if (upperQuery.startsWith("INSERT"))
|
|
265
|
+
queryType = "INSERT";
|
|
266
|
+
else if (upperQuery.startsWith("UPDATE"))
|
|
267
|
+
queryType = "UPDATE";
|
|
268
|
+
else if (upperQuery.startsWith("DELETE"))
|
|
269
|
+
queryType = "DELETE";
|
|
270
|
+
// Extract tables
|
|
271
|
+
const tables = this.extractTables(normalizedQuery);
|
|
272
|
+
// Analyze query structure
|
|
273
|
+
const hasJoins = /\bJOIN\b/i.test(normalizedQuery);
|
|
274
|
+
const hasSubqueries = /\(\s*SELECT\b/i.test(normalizedQuery);
|
|
275
|
+
const hasGroupBy = /\bGROUP\s+BY\b/i.test(normalizedQuery);
|
|
276
|
+
const hasOrderBy = /\bORDER\s+BY\b/i.test(normalizedQuery);
|
|
277
|
+
const hasLimit = /\bLIMIT\b/i.test(normalizedQuery);
|
|
278
|
+
// Estimate complexity
|
|
279
|
+
let complexity = "LOW";
|
|
280
|
+
if (hasSubqueries || (hasJoins && tables.length > 3)) {
|
|
281
|
+
complexity = "HIGH";
|
|
282
|
+
}
|
|
283
|
+
else if (hasJoins || hasGroupBy) {
|
|
284
|
+
complexity = "MEDIUM";
|
|
285
|
+
}
|
|
286
|
+
// Generate suggestions
|
|
287
|
+
const suggestions = this.generateSuggestions({
|
|
288
|
+
queryType,
|
|
289
|
+
tables,
|
|
290
|
+
hasJoins,
|
|
291
|
+
hasSubqueries,
|
|
292
|
+
hasGroupBy,
|
|
293
|
+
hasOrderBy,
|
|
294
|
+
hasLimit,
|
|
295
|
+
query: normalizedQuery,
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
originalQuery: normalizedQuery,
|
|
299
|
+
queryType,
|
|
300
|
+
tables,
|
|
301
|
+
hasJoins,
|
|
302
|
+
hasSubqueries,
|
|
303
|
+
hasGroupBy,
|
|
304
|
+
hasOrderBy,
|
|
305
|
+
hasLimit,
|
|
306
|
+
estimatedComplexity: complexity,
|
|
307
|
+
suggestions,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Extract table names from a query
|
|
312
|
+
*/
|
|
313
|
+
extractTables(query) {
|
|
314
|
+
const tables = new Set();
|
|
315
|
+
// Match FROM clause
|
|
316
|
+
const fromMatch = query.match(/FROM\s+([^\s,;()]+)/gi);
|
|
317
|
+
if (fromMatch) {
|
|
318
|
+
for (const match of fromMatch) {
|
|
319
|
+
const table = match.replace(/FROM\s+/i, "").replace(/[\`"']/g, "");
|
|
320
|
+
if (table && !this.isKeyword(table)) {
|
|
321
|
+
tables.add(table);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Match JOIN clauses
|
|
326
|
+
const joinMatch = query.match(/JOIN\s+([^\s,;()]+)/gi);
|
|
327
|
+
if (joinMatch) {
|
|
328
|
+
for (const match of joinMatch) {
|
|
329
|
+
const table = match.replace(/JOIN\s+/i, "").replace(/[\`"']/g, "");
|
|
330
|
+
if (table && !this.isKeyword(table)) {
|
|
331
|
+
tables.add(table);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Match UPDATE
|
|
336
|
+
const updateMatch = query.match(/UPDATE\s+([^\s,;()]+)/gi);
|
|
337
|
+
if (updateMatch) {
|
|
338
|
+
for (const match of updateMatch) {
|
|
339
|
+
const table = match.replace(/UPDATE\s+/i, "").replace(/[\`"']/g, "");
|
|
340
|
+
if (table && !this.isKeyword(table)) {
|
|
341
|
+
tables.add(table);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Match INSERT INTO
|
|
346
|
+
const insertMatch = query.match(/INSERT\s+INTO\s+([^\s,;()]+)/gi);
|
|
347
|
+
if (insertMatch) {
|
|
348
|
+
for (const match of insertMatch) {
|
|
349
|
+
const table = match
|
|
350
|
+
.replace(/INSERT\s+INTO\s+/i, "")
|
|
351
|
+
.replace(/[\`"']/g, "");
|
|
352
|
+
if (table && !this.isKeyword(table)) {
|
|
353
|
+
tables.add(table);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Match DELETE FROM
|
|
358
|
+
const deleteMatch = query.match(/DELETE\s+FROM\s+([^\s,;()]+)/gi);
|
|
359
|
+
if (deleteMatch) {
|
|
360
|
+
for (const match of deleteMatch) {
|
|
361
|
+
const table = match
|
|
362
|
+
.replace(/DELETE\s+FROM\s+/i, "")
|
|
363
|
+
.replace(/[\`"']/g, "");
|
|
364
|
+
if (table && !this.isKeyword(table)) {
|
|
365
|
+
tables.add(table);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return Array.from(tables);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Check if a word is a SQL keyword
|
|
373
|
+
*/
|
|
374
|
+
isKeyword(word) {
|
|
375
|
+
const keywords = [
|
|
376
|
+
"SELECT",
|
|
377
|
+
"FROM",
|
|
378
|
+
"WHERE",
|
|
379
|
+
"AND",
|
|
380
|
+
"OR",
|
|
381
|
+
"NOT",
|
|
382
|
+
"IN",
|
|
383
|
+
"LIKE",
|
|
384
|
+
"JOIN",
|
|
385
|
+
"INNER",
|
|
386
|
+
"LEFT",
|
|
387
|
+
"RIGHT",
|
|
388
|
+
"OUTER",
|
|
389
|
+
"CROSS",
|
|
390
|
+
"ON",
|
|
391
|
+
"GROUP",
|
|
392
|
+
"BY",
|
|
393
|
+
"ORDER",
|
|
394
|
+
"HAVING",
|
|
395
|
+
"LIMIT",
|
|
396
|
+
"OFFSET",
|
|
397
|
+
"INSERT",
|
|
398
|
+
"INTO",
|
|
399
|
+
"VALUES",
|
|
400
|
+
"UPDATE",
|
|
401
|
+
"SET",
|
|
402
|
+
"DELETE",
|
|
403
|
+
"CREATE",
|
|
404
|
+
"ALTER",
|
|
405
|
+
"DROP",
|
|
406
|
+
"TABLE",
|
|
407
|
+
"INDEX",
|
|
408
|
+
"VIEW",
|
|
409
|
+
"AS",
|
|
410
|
+
"DISTINCT",
|
|
411
|
+
"ALL",
|
|
412
|
+
"UNION",
|
|
413
|
+
"EXCEPT",
|
|
414
|
+
"INTERSECT",
|
|
415
|
+
];
|
|
416
|
+
return keywords.includes(word.toUpperCase());
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Generate optimization suggestions based on query analysis
|
|
420
|
+
*/
|
|
421
|
+
generateSuggestions(analysis) {
|
|
422
|
+
const suggestions = [];
|
|
423
|
+
// Suggestion: Use index for JOIN operations
|
|
424
|
+
if (analysis.hasJoins && analysis.tables.length > 2) {
|
|
425
|
+
suggestions.push({
|
|
426
|
+
type: "HINT",
|
|
427
|
+
priority: "MEDIUM",
|
|
428
|
+
description: "Consider using HASH_JOIN for large table joins",
|
|
429
|
+
suggestedAction: "Add HASH_JOIN hint for better performance on large datasets",
|
|
430
|
+
hint: { type: "HASH_JOIN" },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// Suggestion: Subquery optimization
|
|
434
|
+
if (analysis.hasSubqueries) {
|
|
435
|
+
suggestions.push({
|
|
436
|
+
type: "HINT",
|
|
437
|
+
priority: "HIGH",
|
|
438
|
+
description: "Subqueries detected - consider semi-join optimization",
|
|
439
|
+
suggestedAction: "Use SEMIJOIN hint or rewrite as JOIN",
|
|
440
|
+
hint: { type: "SEMIJOIN", strategy: "MATERIALIZATION" },
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Suggestion: ORDER BY without LIMIT
|
|
444
|
+
if (analysis.hasOrderBy && !analysis.hasLimit) {
|
|
445
|
+
suggestions.push({
|
|
446
|
+
type: "STRUCTURE",
|
|
447
|
+
priority: "MEDIUM",
|
|
448
|
+
description: "ORDER BY without LIMIT may cause full result set sorting",
|
|
449
|
+
suggestedAction: "Consider adding LIMIT clause to improve performance",
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
// Suggestion: GROUP BY optimization
|
|
453
|
+
if (analysis.hasGroupBy) {
|
|
454
|
+
suggestions.push({
|
|
455
|
+
type: "INDEX",
|
|
456
|
+
priority: "MEDIUM",
|
|
457
|
+
description: "GROUP BY operations benefit from indexes on grouped columns",
|
|
458
|
+
suggestedAction: "Ensure indexes exist on GROUP BY columns",
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
// Suggestion: Multiple table joins
|
|
462
|
+
if (analysis.tables.length >= 3) {
|
|
463
|
+
suggestions.push({
|
|
464
|
+
type: "HINT",
|
|
465
|
+
priority: "MEDIUM",
|
|
466
|
+
description: "Multiple tables joined - join order may impact performance",
|
|
467
|
+
suggestedAction: "Consider using JOIN_ORDER hint if you know the optimal order",
|
|
468
|
+
hint: { type: "JOIN_ORDER", tables: analysis.tables },
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
// Suggestion: Long-running query protection
|
|
472
|
+
if (analysis.hasSubqueries || (analysis.hasJoins && analysis.hasGroupBy)) {
|
|
473
|
+
suggestions.push({
|
|
474
|
+
type: "HINT",
|
|
475
|
+
priority: "LOW",
|
|
476
|
+
description: "Complex query may run long - consider execution time limit",
|
|
477
|
+
suggestedAction: "Add MAX_EXECUTION_TIME hint to prevent runaway queries",
|
|
478
|
+
hint: { type: "MAX_EXECUTION_TIME", value: 30000 },
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return suggestions;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get suggested hints for a specific optimization goal
|
|
485
|
+
*/
|
|
486
|
+
getSuggestedHints(goal) {
|
|
487
|
+
switch (goal) {
|
|
488
|
+
case "SPEED":
|
|
489
|
+
return {
|
|
490
|
+
hints: [{ type: "HASH_JOIN" }, { type: "MRR" }],
|
|
491
|
+
sqlBigResult: true,
|
|
492
|
+
};
|
|
493
|
+
case "MEMORY":
|
|
494
|
+
return {
|
|
495
|
+
hints: [{ type: "NO_HASH_JOIN" }, { type: "NO_BNL" }],
|
|
496
|
+
sqlSmallResult: true,
|
|
497
|
+
};
|
|
498
|
+
case "STABILITY":
|
|
499
|
+
return {
|
|
500
|
+
maxExecutionTime: 30000, // 30 seconds
|
|
501
|
+
hints: [{ type: "JOIN_FIXED_ORDER" }],
|
|
502
|
+
};
|
|
503
|
+
default:
|
|
504
|
+
return {};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
exports.QueryOptimizer = QueryOptimizer;
|
|
509
|
+
exports.default = QueryOptimizer;
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import SecurityLayer from "../security/securityLayer";
|
|
2
|
+
import { QueryHints, QueryAnalysis } from "../optimization/queryOptimizer";
|
|
2
3
|
export declare class QueryTools {
|
|
3
4
|
private db;
|
|
4
5
|
private security;
|
|
6
|
+
private optimizer;
|
|
5
7
|
constructor(security: SecurityLayer);
|
|
6
8
|
/**
|
|
7
|
-
* Execute a safe read-only SELECT query
|
|
9
|
+
* Execute a safe read-only SELECT query with optional optimizer hints
|
|
8
10
|
*/
|
|
9
11
|
runQuery(queryParams: {
|
|
10
12
|
query: string;
|
|
11
13
|
params?: any[];
|
|
14
|
+
hints?: QueryHints;
|
|
15
|
+
useCache?: boolean;
|
|
12
16
|
}): Promise<{
|
|
13
17
|
status: string;
|
|
14
18
|
data?: any[];
|
|
15
19
|
error?: string;
|
|
16
20
|
queryLog?: string;
|
|
21
|
+
optimizedQuery?: string;
|
|
17
22
|
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Analyze a query and get optimization suggestions
|
|
25
|
+
*/
|
|
26
|
+
analyzeQuery(query: string): QueryAnalysis;
|
|
27
|
+
/**
|
|
28
|
+
* Get suggested hints for a specific optimization goal
|
|
29
|
+
*/
|
|
30
|
+
getSuggestedHints(goal: "SPEED" | "MEMORY" | "STABILITY"): QueryHints;
|
|
18
31
|
/**
|
|
19
32
|
* Execute write operations (INSERT, UPDATE, DELETE) with validation
|
|
20
33
|
* Note: DDL operations are blocked by the security layer for safety
|