noormme 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +89 -21
- package/README.md +195 -578
- package/dist/cjs/cli/commands/analyze.js +4 -1
- package/dist/cjs/cli/commands/generate.js +48 -8
- package/dist/cjs/cli/commands/init.js +54 -14
- package/dist/cjs/cli/commands/inspect.js +10 -3
- package/dist/cjs/cli/commands/migrate.js +38 -2
- package/dist/cjs/cli/commands/optimize.js +4 -1
- package/dist/cjs/cli/commands/status.js +41 -5
- package/dist/cjs/cli/commands/watch.js +4 -1
- package/dist/cjs/cli/index.js +4 -1
- package/dist/cjs/dialect/database-introspector.js +16 -21
- package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/cjs/dynamic/dynamic.d.ts +20 -0
- package/dist/cjs/dynamic/dynamic.js +25 -0
- package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
- package/dist/cjs/edge-runtime/edge-config.js +323 -0
- package/dist/cjs/errors/NoormError.d.ts +27 -4
- package/dist/cjs/errors/NoormError.js +142 -21
- package/dist/cjs/logging/logger.d.ts +7 -2
- package/dist/cjs/logging/logger.js +21 -7
- package/dist/cjs/noormme.d.ts +12 -8
- package/dist/cjs/noormme.js +133 -61
- package/dist/cjs/operation-node/column-node.js +4 -0
- package/dist/cjs/operation-node/identifier-node.js +4 -0
- package/dist/cjs/operation-node/table-node.js +7 -0
- package/dist/cjs/parser/reference-parser.js +5 -0
- package/dist/cjs/parser/table-parser.js +2 -0
- package/dist/cjs/performance/index.d.ts +44 -0
- package/dist/cjs/performance/index.js +64 -0
- package/dist/cjs/performance/query-optimizer.d.ts +134 -0
- package/dist/cjs/performance/query-optimizer.js +391 -0
- package/dist/cjs/performance/services/cache-service.d.ts +177 -0
- package/dist/cjs/performance/services/cache-service.js +415 -0
- package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
- package/dist/cjs/performance/services/connection-factory.js +498 -0
- package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
- package/dist/cjs/performance/services/metrics-collector.js +406 -0
- package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
- package/dist/cjs/performance/utils/query-parser.js +295 -0
- package/dist/cjs/raw-builder/sql.d.ts +73 -26
- package/dist/cjs/raw-builder/sql.js +9 -0
- package/dist/cjs/repository/repository-factory.d.ts +10 -42
- package/dist/cjs/repository/repository-factory.js +276 -394
- package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
- package/dist/cjs/schema/core/utils/name-generator.js +34 -2
- package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
- package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/cjs/schema/test/error-handling.test.js +52 -33
- package/dist/cjs/schema/test/integration.test.js +51 -5
- package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/cjs/sqlite-migration/index.js +35 -2
- package/dist/cjs/testing/test-utils.d.ts +5 -0
- package/dist/cjs/testing/test-utils.js +66 -6
- package/dist/cjs/types/index.d.ts +78 -13
- package/dist/cjs/types/index.js +23 -0
- package/dist/cjs/types/type-generator.d.ts +8 -0
- package/dist/cjs/types/type-generator.js +86 -17
- package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
- package/dist/cjs/util/safe-sql-helpers.js +221 -0
- package/dist/cjs/util/security-validator.d.ts +44 -0
- package/dist/cjs/util/security-validator.js +256 -0
- package/dist/cjs/util/security.d.ts +60 -0
- package/dist/cjs/util/security.js +137 -0
- package/dist/cjs/watch/schema-watcher.js +26 -7
- package/dist/esm/cli/commands/generate.js +10 -6
- package/dist/esm/cli/commands/init.js +15 -11
- package/dist/esm/cli/commands/inspect.js +6 -2
- package/dist/esm/cli/commands/status.js +3 -3
- package/dist/esm/dialect/database-introspector.js +16 -21
- package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/esm/dynamic/dynamic.d.ts +20 -0
- package/dist/esm/dynamic/dynamic.js +25 -0
- package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
- package/dist/esm/edge-runtime/edge-config.js +281 -0
- package/dist/esm/errors/NoormError.d.ts +27 -4
- package/dist/esm/errors/NoormError.js +134 -18
- package/dist/esm/logging/logger.d.ts +7 -2
- package/dist/esm/logging/logger.js +21 -7
- package/dist/esm/noormme.d.ts +12 -8
- package/dist/esm/noormme.js +130 -61
- package/dist/esm/operation-node/column-node.js +4 -0
- package/dist/esm/operation-node/identifier-node.js +4 -0
- package/dist/esm/operation-node/table-node.js +7 -0
- package/dist/esm/parser/reference-parser.js +5 -0
- package/dist/esm/parser/table-parser.js +2 -0
- package/dist/esm/performance/index.d.ts +44 -0
- package/dist/esm/performance/index.js +48 -0
- package/dist/esm/performance/query-optimizer.d.ts +134 -0
- package/dist/esm/performance/query-optimizer.js +387 -0
- package/dist/esm/performance/services/cache-service.d.ts +177 -0
- package/dist/esm/performance/services/cache-service.js +410 -0
- package/dist/esm/performance/services/connection-factory.d.ts +198 -0
- package/dist/esm/performance/services/connection-factory.js +493 -0
- package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
- package/dist/esm/performance/services/metrics-collector.js +402 -0
- package/dist/esm/performance/utils/query-parser.d.ts +123 -0
- package/dist/esm/performance/utils/query-parser.js +292 -0
- package/dist/esm/raw-builder/sql.d.ts +73 -26
- package/dist/esm/raw-builder/sql.js +9 -0
- package/dist/esm/repository/repository-factory.d.ts +10 -42
- package/dist/esm/repository/repository-factory.js +277 -395
- package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
- package/dist/esm/schema/core/utils/name-generator.js +34 -2
- package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/esm/schema/core/utils/type-mapper.js +4 -7
- package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/esm/schema/test/error-handling.test.js +52 -33
- package/dist/esm/schema/test/integration.test.js +18 -5
- package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/esm/testing/test-utils.d.ts +5 -0
- package/dist/esm/testing/test-utils.js +66 -6
- package/dist/esm/types/index.d.ts +78 -13
- package/dist/esm/types/index.js +22 -1
- package/dist/esm/types/type-generator.d.ts +8 -0
- package/dist/esm/types/type-generator.js +86 -17
- package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
- package/dist/esm/util/safe-sql-helpers.js +209 -0
- package/dist/esm/util/security-validator.d.ts +44 -0
- package/dist/esm/util/security-validator.js +246 -0
- package/dist/esm/util/security.d.ts +60 -0
- package/dist/esm/util/security.js +114 -0
- package/dist/esm/watch/schema-watcher.js +26 -7
- package/package.json +1 -1
- package/dist/cjs/performance/query-analyzer.d.ts +0 -89
- package/dist/cjs/performance/query-analyzer.js +0 -263
- package/dist/esm/performance/query-analyzer.d.ts +0 -89
- package/dist/esm/performance/query-analyzer.js +0 -260
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.QueryAnalyzer = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Analyzes query performance and provides warnings for optimization
|
|
6
|
-
*/
|
|
7
|
-
class QueryAnalyzer {
|
|
8
|
-
db;
|
|
9
|
-
logger;
|
|
10
|
-
schemaInfo;
|
|
11
|
-
options;
|
|
12
|
-
queryHistory = [];
|
|
13
|
-
recentQueries = new Map();
|
|
14
|
-
maxHistorySize = 1000;
|
|
15
|
-
constructor(db, logger, schemaInfo, options = {}) {
|
|
16
|
-
this.db = db;
|
|
17
|
-
this.logger = logger;
|
|
18
|
-
this.schemaInfo = schemaInfo;
|
|
19
|
-
this.options = options;
|
|
20
|
-
this.options = {
|
|
21
|
-
enabled: process.env.NODE_ENV === 'development',
|
|
22
|
-
slowQueryThreshold: 1000, // 1 second
|
|
23
|
-
nPlusOneDetection: true,
|
|
24
|
-
missingIndexDetection: true,
|
|
25
|
-
largeResultSetThreshold: 1000,
|
|
26
|
-
...options
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Record a query execution for analysis
|
|
31
|
-
*/
|
|
32
|
-
recordQuery(query, executionTime, resultCount, table) {
|
|
33
|
-
if (!this.options.enabled)
|
|
34
|
-
return;
|
|
35
|
-
const metrics = {
|
|
36
|
-
query: this.normalizeQuery(query),
|
|
37
|
-
executionTime,
|
|
38
|
-
timestamp: new Date(),
|
|
39
|
-
table,
|
|
40
|
-
operation: this.extractOperation(query)
|
|
41
|
-
};
|
|
42
|
-
this.queryHistory.push(metrics);
|
|
43
|
-
// Keep history size under control
|
|
44
|
-
if (this.queryHistory.length > this.maxHistorySize) {
|
|
45
|
-
this.queryHistory = this.queryHistory.slice(-this.maxHistorySize);
|
|
46
|
-
}
|
|
47
|
-
// Track recent queries for N+1 detection
|
|
48
|
-
const normalizedQuery = metrics.query;
|
|
49
|
-
if (!this.recentQueries.has(normalizedQuery)) {
|
|
50
|
-
this.recentQueries.set(normalizedQuery, []);
|
|
51
|
-
}
|
|
52
|
-
this.recentQueries.get(normalizedQuery).push(metrics);
|
|
53
|
-
// Clean up old recent queries (keep last 10 seconds)
|
|
54
|
-
const cutoff = new Date(Date.now() - 10000);
|
|
55
|
-
for (const [query, queries] of this.recentQueries.entries()) {
|
|
56
|
-
const recent = queries.filter(q => q.timestamp >= cutoff);
|
|
57
|
-
if (recent.length === 0) {
|
|
58
|
-
this.recentQueries.delete(query);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
this.recentQueries.set(query, recent);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// Analyze the query
|
|
65
|
-
this.analyzeQuery(metrics, resultCount);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Analyze a query for performance issues
|
|
69
|
-
*/
|
|
70
|
-
analyzeQuery(metrics, resultCount) {
|
|
71
|
-
const warnings = [];
|
|
72
|
-
// Check for slow queries
|
|
73
|
-
if (this.options.slowQueryThreshold && metrics.executionTime > this.options.slowQueryThreshold) {
|
|
74
|
-
warnings.push({
|
|
75
|
-
type: 'slow_query',
|
|
76
|
-
message: `Slow query detected: ${metrics.executionTime}ms`,
|
|
77
|
-
suggestion: 'Consider adding indexes or optimizing the query',
|
|
78
|
-
query: metrics.query,
|
|
79
|
-
table: metrics.table,
|
|
80
|
-
severity: metrics.executionTime > this.options.slowQueryThreshold * 3 ? 'high' : 'medium'
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
// Check for N+1 queries
|
|
84
|
-
if (this.options.nPlusOneDetection) {
|
|
85
|
-
const nPlusOneWarning = this.detectNPlusOne(metrics);
|
|
86
|
-
if (nPlusOneWarning) {
|
|
87
|
-
warnings.push(nPlusOneWarning);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Check for missing indexes
|
|
91
|
-
if (this.options.missingIndexDetection && metrics.table) {
|
|
92
|
-
const missingIndexWarning = this.checkForMissingIndexes(metrics);
|
|
93
|
-
if (missingIndexWarning) {
|
|
94
|
-
warnings.push(missingIndexWarning);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Check for large result sets
|
|
98
|
-
if (resultCount && this.options.largeResultSetThreshold && resultCount > this.options.largeResultSetThreshold) {
|
|
99
|
-
warnings.push({
|
|
100
|
-
type: 'large_result_set',
|
|
101
|
-
message: `Large result set: ${resultCount} rows returned`,
|
|
102
|
-
suggestion: 'Consider using pagination or filtering to reduce result size',
|
|
103
|
-
query: metrics.query,
|
|
104
|
-
table: metrics.table,
|
|
105
|
-
severity: resultCount > this.options.largeResultSetThreshold * 5 ? 'high' : 'medium'
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
// Log warnings
|
|
109
|
-
for (const warning of warnings) {
|
|
110
|
-
this.logWarning(warning);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Detect N+1 query patterns
|
|
115
|
-
*/
|
|
116
|
-
detectNPlusOne(metrics) {
|
|
117
|
-
const recentQueries = this.recentQueries.get(metrics.query) || [];
|
|
118
|
-
// Look for the same query executed multiple times in quick succession
|
|
119
|
-
if (recentQueries.length >= 5) {
|
|
120
|
-
const timeWindow = 5000; // 5 seconds
|
|
121
|
-
const recentInWindow = recentQueries.filter(q => Date.now() - q.timestamp.getTime() < timeWindow);
|
|
122
|
-
if (recentInWindow.length >= 5) {
|
|
123
|
-
return {
|
|
124
|
-
type: 'n_plus_one',
|
|
125
|
-
message: `Potential N+1 query detected: same query executed ${recentInWindow.length} times`,
|
|
126
|
-
suggestion: 'Consider using joins or batch loading to reduce query count',
|
|
127
|
-
query: metrics.query,
|
|
128
|
-
table: metrics.table,
|
|
129
|
-
severity: 'high'
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Check for missing indexes based on WHERE clauses
|
|
137
|
-
*/
|
|
138
|
-
checkForMissingIndexes(metrics) {
|
|
139
|
-
if (!metrics.table || !metrics.query.includes('WHERE')) {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const table = this.schemaInfo.tables.find(t => t.name === metrics.table);
|
|
143
|
-
if (!table)
|
|
144
|
-
return null;
|
|
145
|
-
// Extract WHERE conditions (simplified parsing)
|
|
146
|
-
const whereMatch = metrics.query.match(/WHERE\s+([^ORDER\s]+)/i);
|
|
147
|
-
if (!whereMatch)
|
|
148
|
-
return null;
|
|
149
|
-
const whereClause = whereMatch[1];
|
|
150
|
-
const columnMatches = whereClause.match(/(\w+)\s*[=<>]/g);
|
|
151
|
-
if (!columnMatches)
|
|
152
|
-
return null;
|
|
153
|
-
// Check if columns used in WHERE have indexes
|
|
154
|
-
for (const match of columnMatches) {
|
|
155
|
-
const column = match.replace(/\s*[=<>].*/, '').trim();
|
|
156
|
-
// Skip if it's the primary key (automatically indexed)
|
|
157
|
-
if (table.primaryKey?.includes(column))
|
|
158
|
-
continue;
|
|
159
|
-
// Check if there's an index on this column
|
|
160
|
-
const hasIndex = table.indexes.some(index => index.columns.includes(column) || index.columns[0] === column);
|
|
161
|
-
if (!hasIndex) {
|
|
162
|
-
return {
|
|
163
|
-
type: 'missing_index',
|
|
164
|
-
message: `Column '${column}' used in WHERE clause may benefit from an index`,
|
|
165
|
-
suggestion: `Consider adding an index on ${metrics.table}.${column}`,
|
|
166
|
-
query: metrics.query,
|
|
167
|
-
table: metrics.table,
|
|
168
|
-
severity: 'medium'
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Normalize query for comparison (remove dynamic values)
|
|
176
|
-
*/
|
|
177
|
-
normalizeQuery(query) {
|
|
178
|
-
return query
|
|
179
|
-
.replace(/\$\d+/g, '?') // Replace PostgreSQL parameters
|
|
180
|
-
.replace(/'[^']*'/g, '?') // Replace string literals
|
|
181
|
-
.replace(/\b\d+\b/g, '?') // Replace numbers
|
|
182
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
183
|
-
.trim();
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Extract operation type from query
|
|
187
|
-
*/
|
|
188
|
-
extractOperation(query) {
|
|
189
|
-
const normalized = query.trim().toUpperCase();
|
|
190
|
-
if (normalized.startsWith('SELECT'))
|
|
191
|
-
return 'SELECT';
|
|
192
|
-
if (normalized.startsWith('INSERT'))
|
|
193
|
-
return 'INSERT';
|
|
194
|
-
if (normalized.startsWith('UPDATE'))
|
|
195
|
-
return 'UPDATE';
|
|
196
|
-
if (normalized.startsWith('DELETE'))
|
|
197
|
-
return 'DELETE';
|
|
198
|
-
return 'OTHER';
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Log performance warning
|
|
202
|
-
*/
|
|
203
|
-
logWarning(warning) {
|
|
204
|
-
const emoji = this.getWarningEmoji(warning.type);
|
|
205
|
-
const message = `${emoji} Performance Warning [${warning.severity.toUpperCase()}]: ${warning.message}`;
|
|
206
|
-
switch (warning.severity) {
|
|
207
|
-
case 'high':
|
|
208
|
-
this.logger.warn(message);
|
|
209
|
-
break;
|
|
210
|
-
case 'medium':
|
|
211
|
-
this.logger.info(message);
|
|
212
|
-
break;
|
|
213
|
-
case 'low':
|
|
214
|
-
this.logger.debug(message);
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
this.logger.debug(` Suggestion: ${warning.suggestion}`);
|
|
218
|
-
if (warning.query) {
|
|
219
|
-
this.logger.debug(` Query: ${warning.query}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Get emoji for warning type
|
|
224
|
-
*/
|
|
225
|
-
getWarningEmoji(type) {
|
|
226
|
-
const emojis = {
|
|
227
|
-
'n_plus_one': '🔄',
|
|
228
|
-
'missing_index': '📇',
|
|
229
|
-
'slow_query': '🐌',
|
|
230
|
-
'full_table_scan': '🔍',
|
|
231
|
-
'large_result_set': '📊'
|
|
232
|
-
};
|
|
233
|
-
return emojis[type] || '⚠️';
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Get performance statistics
|
|
237
|
-
*/
|
|
238
|
-
getPerformanceStats() {
|
|
239
|
-
const slowQueries = this.queryHistory.filter(q => q.executionTime > (this.options.slowQueryThreshold || 1000)).length;
|
|
240
|
-
const totalTime = this.queryHistory.reduce((sum, q) => sum + q.executionTime, 0);
|
|
241
|
-
const averageExecutionTime = this.queryHistory.length > 0 ? totalTime / this.queryHistory.length : 0;
|
|
242
|
-
return {
|
|
243
|
-
totalQueries: this.queryHistory.length,
|
|
244
|
-
averageExecutionTime,
|
|
245
|
-
slowQueries,
|
|
246
|
-
warningCount: {} // Would be populated by tracking warnings
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Clear query history
|
|
251
|
-
*/
|
|
252
|
-
clearHistory() {
|
|
253
|
-
this.queryHistory = [];
|
|
254
|
-
this.recentQueries.clear();
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Update schema info for index checking
|
|
258
|
-
*/
|
|
259
|
-
updateSchema(schemaInfo) {
|
|
260
|
-
this.schemaInfo = schemaInfo;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
exports.QueryAnalyzer = QueryAnalyzer;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from '../kysely.js';
|
|
2
|
-
import { Logger } from '../logging/logger.js';
|
|
3
|
-
import { SchemaInfo } from '../types/index.js';
|
|
4
|
-
export interface QueryMetrics {
|
|
5
|
-
query: string;
|
|
6
|
-
executionTime: number;
|
|
7
|
-
timestamp: Date;
|
|
8
|
-
table?: string;
|
|
9
|
-
operation?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface PerformanceWarning {
|
|
12
|
-
type: 'n_plus_one' | 'missing_index' | 'slow_query' | 'full_table_scan' | 'large_result_set';
|
|
13
|
-
message: string;
|
|
14
|
-
suggestion: string;
|
|
15
|
-
query?: string;
|
|
16
|
-
table?: string;
|
|
17
|
-
severity: 'low' | 'medium' | 'high';
|
|
18
|
-
}
|
|
19
|
-
export interface QueryAnalyzerOptions {
|
|
20
|
-
enabled?: boolean;
|
|
21
|
-
slowQueryThreshold?: number;
|
|
22
|
-
nPlusOneDetection?: boolean;
|
|
23
|
-
missingIndexDetection?: boolean;
|
|
24
|
-
largeResultSetThreshold?: number;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Analyzes query performance and provides warnings for optimization
|
|
28
|
-
*/
|
|
29
|
-
export declare class QueryAnalyzer {
|
|
30
|
-
private db;
|
|
31
|
-
private logger;
|
|
32
|
-
private schemaInfo;
|
|
33
|
-
private options;
|
|
34
|
-
private queryHistory;
|
|
35
|
-
private recentQueries;
|
|
36
|
-
private maxHistorySize;
|
|
37
|
-
constructor(db: Kysely<any>, logger: Logger, schemaInfo: SchemaInfo, options?: QueryAnalyzerOptions);
|
|
38
|
-
/**
|
|
39
|
-
* Record a query execution for analysis
|
|
40
|
-
*/
|
|
41
|
-
recordQuery(query: string, executionTime: number, resultCount?: number, table?: string): void;
|
|
42
|
-
/**
|
|
43
|
-
* Analyze a query for performance issues
|
|
44
|
-
*/
|
|
45
|
-
private analyzeQuery;
|
|
46
|
-
/**
|
|
47
|
-
* Detect N+1 query patterns
|
|
48
|
-
*/
|
|
49
|
-
private detectNPlusOne;
|
|
50
|
-
/**
|
|
51
|
-
* Check for missing indexes based on WHERE clauses
|
|
52
|
-
*/
|
|
53
|
-
private checkForMissingIndexes;
|
|
54
|
-
/**
|
|
55
|
-
* Normalize query for comparison (remove dynamic values)
|
|
56
|
-
*/
|
|
57
|
-
private normalizeQuery;
|
|
58
|
-
/**
|
|
59
|
-
* Extract operation type from query
|
|
60
|
-
*/
|
|
61
|
-
private extractOperation;
|
|
62
|
-
/**
|
|
63
|
-
* Log performance warning
|
|
64
|
-
*/
|
|
65
|
-
private logWarning;
|
|
66
|
-
/**
|
|
67
|
-
* Get emoji for warning type
|
|
68
|
-
*/
|
|
69
|
-
private getWarningEmoji;
|
|
70
|
-
/**
|
|
71
|
-
* Get performance statistics
|
|
72
|
-
*/
|
|
73
|
-
getPerformanceStats(): {
|
|
74
|
-
totalQueries: number;
|
|
75
|
-
averageExecutionTime: number;
|
|
76
|
-
slowQueries: number;
|
|
77
|
-
warningCount: {
|
|
78
|
-
[key: string]: number;
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* Clear query history
|
|
83
|
-
*/
|
|
84
|
-
clearHistory(): void;
|
|
85
|
-
/**
|
|
86
|
-
* Update schema info for index checking
|
|
87
|
-
*/
|
|
88
|
-
updateSchema(schemaInfo: SchemaInfo): void;
|
|
89
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/// <reference types="./query-analyzer.d.ts" />
|
|
2
|
-
/**
|
|
3
|
-
* Analyzes query performance and provides warnings for optimization
|
|
4
|
-
*/
|
|
5
|
-
export class QueryAnalyzer {
|
|
6
|
-
db;
|
|
7
|
-
logger;
|
|
8
|
-
schemaInfo;
|
|
9
|
-
options;
|
|
10
|
-
queryHistory = [];
|
|
11
|
-
recentQueries = new Map();
|
|
12
|
-
maxHistorySize = 1000;
|
|
13
|
-
constructor(db, logger, schemaInfo, options = {}) {
|
|
14
|
-
this.db = db;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
this.schemaInfo = schemaInfo;
|
|
17
|
-
this.options = options;
|
|
18
|
-
this.options = {
|
|
19
|
-
enabled: process.env.NODE_ENV === 'development',
|
|
20
|
-
slowQueryThreshold: 1000, // 1 second
|
|
21
|
-
nPlusOneDetection: true,
|
|
22
|
-
missingIndexDetection: true,
|
|
23
|
-
largeResultSetThreshold: 1000,
|
|
24
|
-
...options
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Record a query execution for analysis
|
|
29
|
-
*/
|
|
30
|
-
recordQuery(query, executionTime, resultCount, table) {
|
|
31
|
-
if (!this.options.enabled)
|
|
32
|
-
return;
|
|
33
|
-
const metrics = {
|
|
34
|
-
query: this.normalizeQuery(query),
|
|
35
|
-
executionTime,
|
|
36
|
-
timestamp: new Date(),
|
|
37
|
-
table,
|
|
38
|
-
operation: this.extractOperation(query)
|
|
39
|
-
};
|
|
40
|
-
this.queryHistory.push(metrics);
|
|
41
|
-
// Keep history size under control
|
|
42
|
-
if (this.queryHistory.length > this.maxHistorySize) {
|
|
43
|
-
this.queryHistory = this.queryHistory.slice(-this.maxHistorySize);
|
|
44
|
-
}
|
|
45
|
-
// Track recent queries for N+1 detection
|
|
46
|
-
const normalizedQuery = metrics.query;
|
|
47
|
-
if (!this.recentQueries.has(normalizedQuery)) {
|
|
48
|
-
this.recentQueries.set(normalizedQuery, []);
|
|
49
|
-
}
|
|
50
|
-
this.recentQueries.get(normalizedQuery).push(metrics);
|
|
51
|
-
// Clean up old recent queries (keep last 10 seconds)
|
|
52
|
-
const cutoff = new Date(Date.now() - 10000);
|
|
53
|
-
for (const [query, queries] of this.recentQueries.entries()) {
|
|
54
|
-
const recent = queries.filter(q => q.timestamp >= cutoff);
|
|
55
|
-
if (recent.length === 0) {
|
|
56
|
-
this.recentQueries.delete(query);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
this.recentQueries.set(query, recent);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// Analyze the query
|
|
63
|
-
this.analyzeQuery(metrics, resultCount);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Analyze a query for performance issues
|
|
67
|
-
*/
|
|
68
|
-
analyzeQuery(metrics, resultCount) {
|
|
69
|
-
const warnings = [];
|
|
70
|
-
// Check for slow queries
|
|
71
|
-
if (this.options.slowQueryThreshold && metrics.executionTime > this.options.slowQueryThreshold) {
|
|
72
|
-
warnings.push({
|
|
73
|
-
type: 'slow_query',
|
|
74
|
-
message: `Slow query detected: ${metrics.executionTime}ms`,
|
|
75
|
-
suggestion: 'Consider adding indexes or optimizing the query',
|
|
76
|
-
query: metrics.query,
|
|
77
|
-
table: metrics.table,
|
|
78
|
-
severity: metrics.executionTime > this.options.slowQueryThreshold * 3 ? 'high' : 'medium'
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
// Check for N+1 queries
|
|
82
|
-
if (this.options.nPlusOneDetection) {
|
|
83
|
-
const nPlusOneWarning = this.detectNPlusOne(metrics);
|
|
84
|
-
if (nPlusOneWarning) {
|
|
85
|
-
warnings.push(nPlusOneWarning);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Check for missing indexes
|
|
89
|
-
if (this.options.missingIndexDetection && metrics.table) {
|
|
90
|
-
const missingIndexWarning = this.checkForMissingIndexes(metrics);
|
|
91
|
-
if (missingIndexWarning) {
|
|
92
|
-
warnings.push(missingIndexWarning);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Check for large result sets
|
|
96
|
-
if (resultCount && this.options.largeResultSetThreshold && resultCount > this.options.largeResultSetThreshold) {
|
|
97
|
-
warnings.push({
|
|
98
|
-
type: 'large_result_set',
|
|
99
|
-
message: `Large result set: ${resultCount} rows returned`,
|
|
100
|
-
suggestion: 'Consider using pagination or filtering to reduce result size',
|
|
101
|
-
query: metrics.query,
|
|
102
|
-
table: metrics.table,
|
|
103
|
-
severity: resultCount > this.options.largeResultSetThreshold * 5 ? 'high' : 'medium'
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
// Log warnings
|
|
107
|
-
for (const warning of warnings) {
|
|
108
|
-
this.logWarning(warning);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Detect N+1 query patterns
|
|
113
|
-
*/
|
|
114
|
-
detectNPlusOne(metrics) {
|
|
115
|
-
const recentQueries = this.recentQueries.get(metrics.query) || [];
|
|
116
|
-
// Look for the same query executed multiple times in quick succession
|
|
117
|
-
if (recentQueries.length >= 5) {
|
|
118
|
-
const timeWindow = 5000; // 5 seconds
|
|
119
|
-
const recentInWindow = recentQueries.filter(q => Date.now() - q.timestamp.getTime() < timeWindow);
|
|
120
|
-
if (recentInWindow.length >= 5) {
|
|
121
|
-
return {
|
|
122
|
-
type: 'n_plus_one',
|
|
123
|
-
message: `Potential N+1 query detected: same query executed ${recentInWindow.length} times`,
|
|
124
|
-
suggestion: 'Consider using joins or batch loading to reduce query count',
|
|
125
|
-
query: metrics.query,
|
|
126
|
-
table: metrics.table,
|
|
127
|
-
severity: 'high'
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Check for missing indexes based on WHERE clauses
|
|
135
|
-
*/
|
|
136
|
-
checkForMissingIndexes(metrics) {
|
|
137
|
-
if (!metrics.table || !metrics.query.includes('WHERE')) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
const table = this.schemaInfo.tables.find(t => t.name === metrics.table);
|
|
141
|
-
if (!table)
|
|
142
|
-
return null;
|
|
143
|
-
// Extract WHERE conditions (simplified parsing)
|
|
144
|
-
const whereMatch = metrics.query.match(/WHERE\s+([^ORDER\s]+)/i);
|
|
145
|
-
if (!whereMatch)
|
|
146
|
-
return null;
|
|
147
|
-
const whereClause = whereMatch[1];
|
|
148
|
-
const columnMatches = whereClause.match(/(\w+)\s*[=<>]/g);
|
|
149
|
-
if (!columnMatches)
|
|
150
|
-
return null;
|
|
151
|
-
// Check if columns used in WHERE have indexes
|
|
152
|
-
for (const match of columnMatches) {
|
|
153
|
-
const column = match.replace(/\s*[=<>].*/, '').trim();
|
|
154
|
-
// Skip if it's the primary key (automatically indexed)
|
|
155
|
-
if (table.primaryKey?.includes(column))
|
|
156
|
-
continue;
|
|
157
|
-
// Check if there's an index on this column
|
|
158
|
-
const hasIndex = table.indexes.some(index => index.columns.includes(column) || index.columns[0] === column);
|
|
159
|
-
if (!hasIndex) {
|
|
160
|
-
return {
|
|
161
|
-
type: 'missing_index',
|
|
162
|
-
message: `Column '${column}' used in WHERE clause may benefit from an index`,
|
|
163
|
-
suggestion: `Consider adding an index on ${metrics.table}.${column}`,
|
|
164
|
-
query: metrics.query,
|
|
165
|
-
table: metrics.table,
|
|
166
|
-
severity: 'medium'
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Normalize query for comparison (remove dynamic values)
|
|
174
|
-
*/
|
|
175
|
-
normalizeQuery(query) {
|
|
176
|
-
return query
|
|
177
|
-
.replace(/\$\d+/g, '?') // Replace PostgreSQL parameters
|
|
178
|
-
.replace(/'[^']*'/g, '?') // Replace string literals
|
|
179
|
-
.replace(/\b\d+\b/g, '?') // Replace numbers
|
|
180
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
181
|
-
.trim();
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Extract operation type from query
|
|
185
|
-
*/
|
|
186
|
-
extractOperation(query) {
|
|
187
|
-
const normalized = query.trim().toUpperCase();
|
|
188
|
-
if (normalized.startsWith('SELECT'))
|
|
189
|
-
return 'SELECT';
|
|
190
|
-
if (normalized.startsWith('INSERT'))
|
|
191
|
-
return 'INSERT';
|
|
192
|
-
if (normalized.startsWith('UPDATE'))
|
|
193
|
-
return 'UPDATE';
|
|
194
|
-
if (normalized.startsWith('DELETE'))
|
|
195
|
-
return 'DELETE';
|
|
196
|
-
return 'OTHER';
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Log performance warning
|
|
200
|
-
*/
|
|
201
|
-
logWarning(warning) {
|
|
202
|
-
const emoji = this.getWarningEmoji(warning.type);
|
|
203
|
-
const message = `${emoji} Performance Warning [${warning.severity.toUpperCase()}]: ${warning.message}`;
|
|
204
|
-
switch (warning.severity) {
|
|
205
|
-
case 'high':
|
|
206
|
-
this.logger.warn(message);
|
|
207
|
-
break;
|
|
208
|
-
case 'medium':
|
|
209
|
-
this.logger.info(message);
|
|
210
|
-
break;
|
|
211
|
-
case 'low':
|
|
212
|
-
this.logger.debug(message);
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
this.logger.debug(` Suggestion: ${warning.suggestion}`);
|
|
216
|
-
if (warning.query) {
|
|
217
|
-
this.logger.debug(` Query: ${warning.query}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Get emoji for warning type
|
|
222
|
-
*/
|
|
223
|
-
getWarningEmoji(type) {
|
|
224
|
-
const emojis = {
|
|
225
|
-
'n_plus_one': '🔄',
|
|
226
|
-
'missing_index': '📇',
|
|
227
|
-
'slow_query': '🐌',
|
|
228
|
-
'full_table_scan': '🔍',
|
|
229
|
-
'large_result_set': '📊'
|
|
230
|
-
};
|
|
231
|
-
return emojis[type] || '⚠️';
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Get performance statistics
|
|
235
|
-
*/
|
|
236
|
-
getPerformanceStats() {
|
|
237
|
-
const slowQueries = this.queryHistory.filter(q => q.executionTime > (this.options.slowQueryThreshold || 1000)).length;
|
|
238
|
-
const totalTime = this.queryHistory.reduce((sum, q) => sum + q.executionTime, 0);
|
|
239
|
-
const averageExecutionTime = this.queryHistory.length > 0 ? totalTime / this.queryHistory.length : 0;
|
|
240
|
-
return {
|
|
241
|
-
totalQueries: this.queryHistory.length,
|
|
242
|
-
averageExecutionTime,
|
|
243
|
-
slowQueries,
|
|
244
|
-
warningCount: {} // Would be populated by tracking warnings
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Clear query history
|
|
249
|
-
*/
|
|
250
|
-
clearHistory() {
|
|
251
|
-
this.queryHistory = [];
|
|
252
|
-
this.recentQueries.clear();
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Update schema info for index checking
|
|
256
|
-
*/
|
|
257
|
-
updateSchema(schemaInfo) {
|
|
258
|
-
this.schemaInfo = schemaInfo;
|
|
259
|
-
}
|
|
260
|
-
}
|