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
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsCollector = void 0;
|
|
4
|
+
exports.createMetricsCollector = createMetricsCollector;
|
|
5
|
+
const logger_js_1 = require("../../logging/logger.js");
|
|
6
|
+
/**
|
|
7
|
+
* Service for collecting and analyzing performance metrics
|
|
8
|
+
*/
|
|
9
|
+
class MetricsCollector {
|
|
10
|
+
queryHistory = [];
|
|
11
|
+
warnings = [];
|
|
12
|
+
recentQueries = new Map();
|
|
13
|
+
config;
|
|
14
|
+
logger;
|
|
15
|
+
cleanupTimer;
|
|
16
|
+
constructor(config = {}, logger) {
|
|
17
|
+
this.logger = logger || new logger_js_1.Logger('MetricsCollector');
|
|
18
|
+
this.config = {
|
|
19
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
20
|
+
slowQueryThreshold: 1000, // 1 second
|
|
21
|
+
nPlusOneDetection: true,
|
|
22
|
+
missingIndexDetection: true,
|
|
23
|
+
largeResultSetThreshold: 1000,
|
|
24
|
+
maxHistorySize: 10000,
|
|
25
|
+
warningRetentionDays: 7,
|
|
26
|
+
...config
|
|
27
|
+
};
|
|
28
|
+
this.startCleanupTimer();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Record a query execution
|
|
32
|
+
*/
|
|
33
|
+
recordQuery(query, executionTime, options = {}) {
|
|
34
|
+
if (!this.config.enabled)
|
|
35
|
+
return;
|
|
36
|
+
const metrics = {
|
|
37
|
+
query: this.normalizeQuery(query),
|
|
38
|
+
executionTime,
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
table: options.table,
|
|
41
|
+
operation: options.operation,
|
|
42
|
+
resultCount: options.resultCount,
|
|
43
|
+
error: options.error
|
|
44
|
+
};
|
|
45
|
+
this.queryHistory.push(metrics);
|
|
46
|
+
// Keep history size under control
|
|
47
|
+
if (this.queryHistory.length > this.config.maxHistorySize) {
|
|
48
|
+
this.queryHistory = this.queryHistory.slice(-this.config.maxHistorySize);
|
|
49
|
+
}
|
|
50
|
+
// Track recent queries for pattern detection
|
|
51
|
+
this.trackRecentQuery(metrics);
|
|
52
|
+
// Analyze for warnings
|
|
53
|
+
this.analyzeQuery(metrics);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get performance statistics
|
|
57
|
+
*/
|
|
58
|
+
getPerformanceStats() {
|
|
59
|
+
const totalQueries = this.queryHistory.length;
|
|
60
|
+
const slowQueries = this.queryHistory.filter(q => q.executionTime > this.config.slowQueryThreshold).length;
|
|
61
|
+
const averageExecutionTime = totalQueries > 0
|
|
62
|
+
? this.queryHistory.reduce((sum, q) => sum + q.executionTime, 0) / totalQueries
|
|
63
|
+
: 0;
|
|
64
|
+
const errorRate = totalQueries > 0
|
|
65
|
+
? this.queryHistory.filter(q => q.error).length / totalQueries
|
|
66
|
+
: 0;
|
|
67
|
+
const warningCount = {};
|
|
68
|
+
for (const warning of this.warnings) {
|
|
69
|
+
warningCount[warning.type] = (warningCount[warning.type] || 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
const topSlowQueries = this.getTopSlowQueries();
|
|
72
|
+
const queryPatterns = this.getQueryPatterns();
|
|
73
|
+
return {
|
|
74
|
+
totalQueries,
|
|
75
|
+
averageExecutionTime,
|
|
76
|
+
slowQueries,
|
|
77
|
+
errorRate,
|
|
78
|
+
warningCount,
|
|
79
|
+
topSlowQueries,
|
|
80
|
+
queryPatterns
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get recent warnings
|
|
85
|
+
*/
|
|
86
|
+
getRecentWarnings(limit = 50) {
|
|
87
|
+
return this.warnings
|
|
88
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
89
|
+
.slice(0, limit);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get warnings by type
|
|
93
|
+
*/
|
|
94
|
+
getWarningsByType(type) {
|
|
95
|
+
return this.warnings.filter(w => w.type === type);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get query history
|
|
99
|
+
*/
|
|
100
|
+
getQueryHistory(limit) {
|
|
101
|
+
const history = this.queryHistory
|
|
102
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
103
|
+
return limit ? history.slice(0, limit) : history;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get slow queries
|
|
107
|
+
*/
|
|
108
|
+
getSlowQueries(threshold) {
|
|
109
|
+
const actualThreshold = threshold || this.config.slowQueryThreshold;
|
|
110
|
+
return this.queryHistory
|
|
111
|
+
.filter(q => q.executionTime > actualThreshold)
|
|
112
|
+
.sort((a, b) => b.executionTime - a.executionTime);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get N+1 query patterns
|
|
116
|
+
*/
|
|
117
|
+
getNPlusOnePatterns() {
|
|
118
|
+
const patterns = new Map();
|
|
119
|
+
// Group queries by normalized pattern
|
|
120
|
+
for (const query of this.queryHistory) {
|
|
121
|
+
if (!patterns.has(query.query)) {
|
|
122
|
+
patterns.set(query.query, []);
|
|
123
|
+
}
|
|
124
|
+
patterns.get(query.query).push(query);
|
|
125
|
+
}
|
|
126
|
+
const nPlusOnePatterns = [];
|
|
127
|
+
// Analyze each pattern for N+1 behavior
|
|
128
|
+
for (const [pattern, queries] of patterns.entries()) {
|
|
129
|
+
if (queries.length < 5)
|
|
130
|
+
continue;
|
|
131
|
+
const timeWindows = [5000, 10000, 30000]; // 5s, 10s, 30s
|
|
132
|
+
for (const windowMs of timeWindows) {
|
|
133
|
+
let maxCountInWindow = 0;
|
|
134
|
+
for (const query of queries) {
|
|
135
|
+
const windowStart = query.timestamp - windowMs;
|
|
136
|
+
const countInWindow = queries.filter(q => q.timestamp >= windowStart && q.timestamp <= query.timestamp).length;
|
|
137
|
+
maxCountInWindow = Math.max(maxCountInWindow, countInWindow);
|
|
138
|
+
}
|
|
139
|
+
if (maxCountInWindow >= 5) {
|
|
140
|
+
let severity = 'low';
|
|
141
|
+
if (maxCountInWindow >= 20)
|
|
142
|
+
severity = 'high';
|
|
143
|
+
else if (maxCountInWindow >= 10)
|
|
144
|
+
severity = 'medium';
|
|
145
|
+
nPlusOnePatterns.push({
|
|
146
|
+
pattern,
|
|
147
|
+
count: maxCountInWindow,
|
|
148
|
+
timeWindow: windowMs,
|
|
149
|
+
severity
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return nPlusOnePatterns.sort((a, b) => b.count - a.count);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear all metrics
|
|
159
|
+
*/
|
|
160
|
+
clear() {
|
|
161
|
+
this.queryHistory = [];
|
|
162
|
+
this.warnings = [];
|
|
163
|
+
this.recentQueries.clear();
|
|
164
|
+
this.logger.info('Metrics cleared');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Export metrics data
|
|
168
|
+
*/
|
|
169
|
+
exportMetrics() {
|
|
170
|
+
return {
|
|
171
|
+
config: this.config,
|
|
172
|
+
stats: this.getPerformanceStats(),
|
|
173
|
+
warnings: this.warnings,
|
|
174
|
+
queries: this.queryHistory
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Shutdown metrics collector
|
|
179
|
+
*/
|
|
180
|
+
shutdown() {
|
|
181
|
+
if (this.cleanupTimer) {
|
|
182
|
+
clearInterval(this.cleanupTimer);
|
|
183
|
+
}
|
|
184
|
+
this.logger.info('Metrics collector shutdown');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Track recent query for pattern detection
|
|
188
|
+
*/
|
|
189
|
+
trackRecentQuery(metrics) {
|
|
190
|
+
const normalizedQuery = metrics.query;
|
|
191
|
+
if (!this.recentQueries.has(normalizedQuery)) {
|
|
192
|
+
this.recentQueries.set(normalizedQuery, []);
|
|
193
|
+
}
|
|
194
|
+
this.recentQueries.get(normalizedQuery).push(metrics);
|
|
195
|
+
// Clean up old recent queries (keep last 10 seconds)
|
|
196
|
+
const cutoff = Date.now() - 10000;
|
|
197
|
+
for (const [query, queries] of this.recentQueries.entries()) {
|
|
198
|
+
const recent = queries.filter(q => q.timestamp >= cutoff);
|
|
199
|
+
if (recent.length === 0) {
|
|
200
|
+
this.recentQueries.delete(query);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
this.recentQueries.set(query, recent);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Analyze query for performance issues
|
|
209
|
+
*/
|
|
210
|
+
analyzeQuery(metrics) {
|
|
211
|
+
const warnings = [];
|
|
212
|
+
// Check for slow queries
|
|
213
|
+
if (metrics.executionTime > this.config.slowQueryThreshold) {
|
|
214
|
+
warnings.push({
|
|
215
|
+
type: 'slow_query',
|
|
216
|
+
message: `Slow query detected: ${metrics.executionTime}ms`,
|
|
217
|
+
suggestion: 'Consider adding indexes or optimizing the query',
|
|
218
|
+
query: metrics.query,
|
|
219
|
+
table: metrics.table,
|
|
220
|
+
severity: metrics.executionTime > this.config.slowQueryThreshold * 3 ? 'high' : 'medium',
|
|
221
|
+
timestamp: Date.now()
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// Check for N+1 queries
|
|
225
|
+
if (this.config.nPlusOneDetection) {
|
|
226
|
+
const nPlusOneWarning = this.detectNPlusOne(metrics);
|
|
227
|
+
if (nPlusOneWarning) {
|
|
228
|
+
warnings.push(nPlusOneWarning);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Check for large result sets
|
|
232
|
+
if (metrics.resultCount && metrics.resultCount > this.config.largeResultSetThreshold) {
|
|
233
|
+
warnings.push({
|
|
234
|
+
type: 'large_result_set',
|
|
235
|
+
message: `Large result set: ${metrics.resultCount} rows returned`,
|
|
236
|
+
suggestion: 'Consider using pagination or filtering to reduce result size',
|
|
237
|
+
query: metrics.query,
|
|
238
|
+
table: metrics.table,
|
|
239
|
+
severity: metrics.resultCount > this.config.largeResultSetThreshold * 5 ? 'high' : 'medium',
|
|
240
|
+
timestamp: Date.now()
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Log warnings
|
|
244
|
+
for (const warning of warnings) {
|
|
245
|
+
this.addWarning(warning);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Detect N+1 query patterns
|
|
250
|
+
*/
|
|
251
|
+
detectNPlusOne(metrics) {
|
|
252
|
+
const recentQueries = this.recentQueries.get(metrics.query) || [];
|
|
253
|
+
// Look for the same query executed multiple times in quick succession
|
|
254
|
+
if (recentQueries.length >= 5) {
|
|
255
|
+
const timeWindow = 5000; // 5 seconds
|
|
256
|
+
const recentInWindow = recentQueries.filter(q => Date.now() - q.timestamp < timeWindow);
|
|
257
|
+
if (recentInWindow.length >= 5) {
|
|
258
|
+
return {
|
|
259
|
+
type: 'n_plus_one',
|
|
260
|
+
message: `Potential N+1 query detected: same query executed ${recentInWindow.length} times`,
|
|
261
|
+
suggestion: 'Consider using joins or batch loading to reduce query count',
|
|
262
|
+
query: metrics.query,
|
|
263
|
+
table: metrics.table,
|
|
264
|
+
severity: 'high',
|
|
265
|
+
timestamp: Date.now()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Add warning to collection
|
|
273
|
+
*/
|
|
274
|
+
addWarning(warning) {
|
|
275
|
+
this.warnings.push(warning);
|
|
276
|
+
this.logWarning(warning);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Log performance warning
|
|
280
|
+
*/
|
|
281
|
+
logWarning(warning) {
|
|
282
|
+
const emoji = this.getWarningEmoji(warning.type);
|
|
283
|
+
const message = `${emoji} Performance Warning [${warning.severity.toUpperCase()}]: ${warning.message}`;
|
|
284
|
+
switch (warning.severity) {
|
|
285
|
+
case 'high':
|
|
286
|
+
this.logger.warn(message);
|
|
287
|
+
break;
|
|
288
|
+
case 'medium':
|
|
289
|
+
this.logger.info(message);
|
|
290
|
+
break;
|
|
291
|
+
case 'low':
|
|
292
|
+
this.logger.debug(message);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
this.logger.debug(` Suggestion: ${warning.suggestion}`);
|
|
296
|
+
if (warning.query) {
|
|
297
|
+
this.logger.debug(` Query: ${warning.query}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get emoji for warning type
|
|
302
|
+
*/
|
|
303
|
+
getWarningEmoji(type) {
|
|
304
|
+
const emojis = {
|
|
305
|
+
'n_plus_one': '🔄',
|
|
306
|
+
'missing_index': '📇',
|
|
307
|
+
'slow_query': '🐌',
|
|
308
|
+
'full_table_scan': '🔍',
|
|
309
|
+
'large_result_set': '📊'
|
|
310
|
+
};
|
|
311
|
+
return emojis[type] || '⚠️';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Normalize query for comparison
|
|
315
|
+
*/
|
|
316
|
+
normalizeQuery(query) {
|
|
317
|
+
return query
|
|
318
|
+
.replace(/\$\d+/g, '?') // Replace PostgreSQL parameters
|
|
319
|
+
.replace(/'[^']*'/g, '?') // Replace string literals
|
|
320
|
+
.replace(/\b\d+\b/g, '?') // Replace numbers
|
|
321
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
322
|
+
.trim();
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get top slow queries
|
|
326
|
+
*/
|
|
327
|
+
getTopSlowQueries() {
|
|
328
|
+
const queryMap = new Map();
|
|
329
|
+
for (const metrics of this.queryHistory) {
|
|
330
|
+
if (metrics.executionTime > this.config.slowQueryThreshold) {
|
|
331
|
+
if (!queryMap.has(metrics.query)) {
|
|
332
|
+
queryMap.set(metrics.query, { totalTime: 0, count: 0 });
|
|
333
|
+
}
|
|
334
|
+
const entry = queryMap.get(metrics.query);
|
|
335
|
+
entry.totalTime += metrics.executionTime;
|
|
336
|
+
entry.count++;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return Array.from(queryMap.entries())
|
|
340
|
+
.map(([query, { totalTime, count }]) => ({
|
|
341
|
+
query,
|
|
342
|
+
averageTime: totalTime / count,
|
|
343
|
+
count
|
|
344
|
+
}))
|
|
345
|
+
.sort((a, b) => b.averageTime - a.averageTime)
|
|
346
|
+
.slice(0, 10);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get query patterns
|
|
350
|
+
*/
|
|
351
|
+
getQueryPatterns() {
|
|
352
|
+
const patternMap = new Map();
|
|
353
|
+
for (const metrics of this.queryHistory) {
|
|
354
|
+
if (!patternMap.has(metrics.query)) {
|
|
355
|
+
patternMap.set(metrics.query, { totalTime: 0, count: 0 });
|
|
356
|
+
}
|
|
357
|
+
const entry = patternMap.get(metrics.query);
|
|
358
|
+
entry.totalTime += metrics.executionTime;
|
|
359
|
+
entry.count++;
|
|
360
|
+
}
|
|
361
|
+
return Array.from(patternMap.entries())
|
|
362
|
+
.map(([pattern, { totalTime, count }]) => ({
|
|
363
|
+
pattern,
|
|
364
|
+
frequency: count,
|
|
365
|
+
averageTime: totalTime / count
|
|
366
|
+
}))
|
|
367
|
+
.sort((a, b) => b.frequency - a.frequency)
|
|
368
|
+
.slice(0, 20);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Start cleanup timer
|
|
372
|
+
*/
|
|
373
|
+
startCleanupTimer() {
|
|
374
|
+
this.cleanupTimer = setInterval(() => {
|
|
375
|
+
this.cleanup();
|
|
376
|
+
}, 60000); // Cleanup every minute
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Cleanup old data
|
|
380
|
+
*/
|
|
381
|
+
cleanup() {
|
|
382
|
+
const cutoffTime = Date.now() - (this.config.warningRetentionDays * 24 * 60 * 60 * 1000);
|
|
383
|
+
// Remove old warnings
|
|
384
|
+
this.warnings = this.warnings.filter(w => w.timestamp > cutoffTime);
|
|
385
|
+
// Remove old queries (keep last 24 hours)
|
|
386
|
+
const queryCutoff = Date.now() - (24 * 60 * 60 * 1000);
|
|
387
|
+
this.queryHistory = this.queryHistory.filter(q => q.timestamp > queryCutoff);
|
|
388
|
+
// Clean up recent queries
|
|
389
|
+
for (const [query, queries] of this.recentQueries.entries()) {
|
|
390
|
+
const recent = queries.filter(q => q.timestamp > queryCutoff);
|
|
391
|
+
if (recent.length === 0) {
|
|
392
|
+
this.recentQueries.delete(query);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
this.recentQueries.set(query, recent);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
exports.MetricsCollector = MetricsCollector;
|
|
401
|
+
/**
|
|
402
|
+
* Factory function to create metrics collector
|
|
403
|
+
*/
|
|
404
|
+
function createMetricsCollector(config, logger) {
|
|
405
|
+
return new MetricsCollector(config, logger);
|
|
406
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query parsing utilities for performance analysis and optimization
|
|
3
|
+
*/
|
|
4
|
+
export interface ParsedQuery {
|
|
5
|
+
type: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'OTHER';
|
|
6
|
+
tables: string[];
|
|
7
|
+
whereColumns: string[];
|
|
8
|
+
joinColumns: Array<{
|
|
9
|
+
table: string;
|
|
10
|
+
column: string;
|
|
11
|
+
}>;
|
|
12
|
+
orderByColumns: string[];
|
|
13
|
+
groupByColumns: string[];
|
|
14
|
+
hasLimit: boolean;
|
|
15
|
+
hasAggregates: boolean;
|
|
16
|
+
isComplex: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface QueryPattern {
|
|
19
|
+
normalized: string;
|
|
20
|
+
original: string;
|
|
21
|
+
frequency: number;
|
|
22
|
+
averageTime: number;
|
|
23
|
+
tables: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Utility class for parsing and analyzing SQL queries
|
|
27
|
+
*/
|
|
28
|
+
export declare class QueryParser {
|
|
29
|
+
/**
|
|
30
|
+
* Parse a SQL query into structured components
|
|
31
|
+
*/
|
|
32
|
+
static parseQuery(query: string): ParsedQuery;
|
|
33
|
+
/**
|
|
34
|
+
* Normalize query for comparison (remove dynamic values)
|
|
35
|
+
*/
|
|
36
|
+
static normalizeQuery(query: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Extract query type from SQL statement
|
|
39
|
+
*/
|
|
40
|
+
static extractQueryType(query: string): ParsedQuery['type'];
|
|
41
|
+
/**
|
|
42
|
+
* Extract table names from query
|
|
43
|
+
*/
|
|
44
|
+
static extractTables(query: string): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Extract columns used in WHERE clauses
|
|
47
|
+
*/
|
|
48
|
+
static extractWhereColumns(query: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Extract columns used in JOIN conditions
|
|
51
|
+
*/
|
|
52
|
+
static extractJoinColumns(query: string): Array<{
|
|
53
|
+
table: string;
|
|
54
|
+
column: string;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Extract columns used in ORDER BY clauses
|
|
58
|
+
*/
|
|
59
|
+
static extractOrderByColumns(query: string): string[];
|
|
60
|
+
/**
|
|
61
|
+
* Extract columns used in GROUP BY clauses
|
|
62
|
+
*/
|
|
63
|
+
static extractGroupByColumns(query: string): string[];
|
|
64
|
+
/**
|
|
65
|
+
* Check if query contains aggregate functions
|
|
66
|
+
*/
|
|
67
|
+
static hasAggregates(query: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Check if query is complex (joins, subqueries, etc.)
|
|
70
|
+
*/
|
|
71
|
+
static isComplexQuery(query: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Check if query could return many rows
|
|
74
|
+
*/
|
|
75
|
+
static couldReturnManyRows(query: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Extract the primary table from a query
|
|
78
|
+
*/
|
|
79
|
+
static extractPrimaryTable(query: string): string | null;
|
|
80
|
+
/**
|
|
81
|
+
* Check if query is a SELECT statement
|
|
82
|
+
*/
|
|
83
|
+
static isSelectQuery(query: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Check if query is a JOIN statement
|
|
86
|
+
*/
|
|
87
|
+
static isJoinQuery(query: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Check if query is an aggregate statement
|
|
90
|
+
*/
|
|
91
|
+
static isAggregateQuery(query: string): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Generate a cache key for a query
|
|
94
|
+
*/
|
|
95
|
+
static generateCacheKey(query: string, params?: any[]): string;
|
|
96
|
+
/**
|
|
97
|
+
* Check if query should be cached
|
|
98
|
+
*/
|
|
99
|
+
static shouldCache(query: string, executionTime: number): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Extract operation type from query
|
|
102
|
+
*/
|
|
103
|
+
static extractOperation(query: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Analyze query patterns from a list of queries
|
|
106
|
+
*/
|
|
107
|
+
static analyzePatterns(queries: Array<{
|
|
108
|
+
query: string;
|
|
109
|
+
executionTime: number;
|
|
110
|
+
}>): QueryPattern[];
|
|
111
|
+
/**
|
|
112
|
+
* Detect potential N+1 query patterns
|
|
113
|
+
*/
|
|
114
|
+
static detectNPlusOnePatterns(queries: Array<{
|
|
115
|
+
query: string;
|
|
116
|
+
timestamp: number;
|
|
117
|
+
}>): Array<{
|
|
118
|
+
pattern: string;
|
|
119
|
+
count: number;
|
|
120
|
+
timeWindow: number;
|
|
121
|
+
severity: 'low' | 'medium' | 'high';
|
|
122
|
+
}>;
|
|
123
|
+
}
|