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,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryOptimizer = void 0;
|
|
4
|
+
exports.createQueryOptimizer = createQueryOptimizer;
|
|
5
|
+
const logger_js_1 = require("../logging/logger.js");
|
|
6
|
+
const query_parser_1 = require("./utils/query-parser");
|
|
7
|
+
const cache_service_1 = require("./services/cache-service");
|
|
8
|
+
const metrics_collector_1 = require("./services/metrics-collector");
|
|
9
|
+
const sql_1 = require("../raw-builder/sql");
|
|
10
|
+
/**
|
|
11
|
+
* Refactored query optimizer using focused services
|
|
12
|
+
*/
|
|
13
|
+
class QueryOptimizer {
|
|
14
|
+
db;
|
|
15
|
+
cacheService;
|
|
16
|
+
metricsCollector;
|
|
17
|
+
options;
|
|
18
|
+
logger;
|
|
19
|
+
schemaInfo;
|
|
20
|
+
constructor(db, schemaInfo, options = {}, logger) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.schemaInfo = schemaInfo;
|
|
23
|
+
this.logger = logger || new logger_js_1.Logger('QueryOptimizer');
|
|
24
|
+
this.options = {
|
|
25
|
+
enableQueryCache: true,
|
|
26
|
+
enableIndexRecommendations: true,
|
|
27
|
+
enableQueryRewriting: true,
|
|
28
|
+
slowQueryThreshold: 1000,
|
|
29
|
+
cacheSize: 1000,
|
|
30
|
+
maxCacheAge: 300000,
|
|
31
|
+
...options
|
|
32
|
+
};
|
|
33
|
+
// Initialize services
|
|
34
|
+
this.cacheService = new cache_service_1.QueryCacheService({
|
|
35
|
+
maxSize: this.options.cacheSize,
|
|
36
|
+
defaultTtl: this.options.maxCacheAge,
|
|
37
|
+
enableMetrics: true
|
|
38
|
+
}, this.logger);
|
|
39
|
+
this.metricsCollector = new metrics_collector_1.MetricsCollector({
|
|
40
|
+
enabled: true,
|
|
41
|
+
slowQueryThreshold: this.options.slowQueryThreshold,
|
|
42
|
+
nPlusOneDetection: true,
|
|
43
|
+
missingIndexDetection: this.options.enableIndexRecommendations,
|
|
44
|
+
largeResultSetThreshold: 1000
|
|
45
|
+
}, this.logger);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Optimize and execute a query
|
|
49
|
+
*/
|
|
50
|
+
async optimizeQuery(query, params = [], context) {
|
|
51
|
+
const startTime = performance.now();
|
|
52
|
+
try {
|
|
53
|
+
// Check cache first
|
|
54
|
+
if (this.options.enableQueryCache) {
|
|
55
|
+
const cachedResult = this.cacheService.getCachedQuery(query, params);
|
|
56
|
+
if (cachedResult) {
|
|
57
|
+
const executionTime = performance.now() - startTime;
|
|
58
|
+
return {
|
|
59
|
+
originalQuery: query,
|
|
60
|
+
optimizedQuery: query,
|
|
61
|
+
optimizationType: 'cache_hit',
|
|
62
|
+
executionTime,
|
|
63
|
+
improvement: 100,
|
|
64
|
+
suggestions: ['Query served from cache'],
|
|
65
|
+
result: cachedResult
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Parse and analyze query
|
|
70
|
+
const parsedQuery = query_parser_1.QueryParser.parseQuery(query);
|
|
71
|
+
const optimization = await this.analyzeQuery(parsedQuery, query, params);
|
|
72
|
+
const optimizedQuery = optimization.optimizedQuery || query;
|
|
73
|
+
// Execute the query
|
|
74
|
+
const result = await this.executeQuery(optimizedQuery, params);
|
|
75
|
+
const executionTime = performance.now() - startTime;
|
|
76
|
+
// Cache the result if appropriate
|
|
77
|
+
if (this.options.enableQueryCache &&
|
|
78
|
+
query_parser_1.QueryParser.shouldCache(query, executionTime)) {
|
|
79
|
+
this.cacheService.cacheQuery(query, params, result);
|
|
80
|
+
}
|
|
81
|
+
// Record metrics
|
|
82
|
+
this.metricsCollector.recordQuery(query, executionTime, {
|
|
83
|
+
table: context?.table,
|
|
84
|
+
operation: context?.operation,
|
|
85
|
+
resultCount: Array.isArray(result) ? result.length : 1
|
|
86
|
+
});
|
|
87
|
+
// Generate suggestions
|
|
88
|
+
const suggestions = this.generateSuggestions(parsedQuery, executionTime, context);
|
|
89
|
+
return {
|
|
90
|
+
originalQuery: query,
|
|
91
|
+
optimizedQuery,
|
|
92
|
+
optimizationType: optimization.type,
|
|
93
|
+
executionTime,
|
|
94
|
+
improvement: optimization.improvement,
|
|
95
|
+
suggestions,
|
|
96
|
+
result
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const executionTime = performance.now() - startTime;
|
|
101
|
+
// Record error metrics
|
|
102
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
103
|
+
this.metricsCollector.recordQuery(query, executionTime, {
|
|
104
|
+
table: context?.table,
|
|
105
|
+
operation: context?.operation,
|
|
106
|
+
error: errorMessage
|
|
107
|
+
});
|
|
108
|
+
this.logger.error(`Query optimization failed: ${errorMessage}`, {
|
|
109
|
+
query,
|
|
110
|
+
executionTime
|
|
111
|
+
});
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get index recommendations
|
|
117
|
+
*/
|
|
118
|
+
async getIndexRecommendations() {
|
|
119
|
+
if (!this.options.enableIndexRecommendations) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const slowQueries = this.metricsCollector.getSlowQueries();
|
|
123
|
+
const recommendations = [];
|
|
124
|
+
for (const queryInfo of slowQueries) {
|
|
125
|
+
const parsedQuery = query_parser_1.QueryParser.parseQuery(queryInfo.query);
|
|
126
|
+
const queryRecommendations = this.analyzeQueryForIndexes(parsedQuery, queryInfo.query);
|
|
127
|
+
recommendations.push(...queryRecommendations);
|
|
128
|
+
}
|
|
129
|
+
return this.deduplicateRecommendations(recommendations);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get query performance statistics
|
|
133
|
+
*/
|
|
134
|
+
getQueryStats() {
|
|
135
|
+
const metricsStats = this.metricsCollector.getPerformanceStats();
|
|
136
|
+
const cacheStats = this.cacheService.getStats();
|
|
137
|
+
return {
|
|
138
|
+
...metricsStats,
|
|
139
|
+
cache: cacheStats,
|
|
140
|
+
indexRecommendations: 0 // Would be populated by recommendations
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clear query cache
|
|
145
|
+
*/
|
|
146
|
+
clearCache() {
|
|
147
|
+
this.cacheService.clear();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Clear metrics
|
|
151
|
+
*/
|
|
152
|
+
clearMetrics() {
|
|
153
|
+
this.metricsCollector.clear();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get cache statistics
|
|
157
|
+
*/
|
|
158
|
+
getCacheStats() {
|
|
159
|
+
return this.cacheService.getStats();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get performance warnings
|
|
163
|
+
*/
|
|
164
|
+
getPerformanceWarnings() {
|
|
165
|
+
return this.metricsCollector.getRecentWarnings();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Shutdown optimizer
|
|
169
|
+
*/
|
|
170
|
+
shutdown() {
|
|
171
|
+
this.cacheService.shutdown();
|
|
172
|
+
this.metricsCollector.shutdown();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Analyze query for optimization opportunities
|
|
176
|
+
*/
|
|
177
|
+
async analyzeQuery(parsedQuery, originalQuery, params) {
|
|
178
|
+
let optimizedQuery = originalQuery;
|
|
179
|
+
let improvement = 0;
|
|
180
|
+
let type = 'no_optimization';
|
|
181
|
+
// Apply query-specific optimizations
|
|
182
|
+
if (parsedQuery.type === 'SELECT') {
|
|
183
|
+
const selectOptimization = this.optimizeSelectQuery(parsedQuery, originalQuery);
|
|
184
|
+
if (selectOptimization.improvement > 0) {
|
|
185
|
+
optimizedQuery = selectOptimization.optimizedQuery;
|
|
186
|
+
improvement = selectOptimization.improvement;
|
|
187
|
+
type = 'query_rewrite';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (parsedQuery.isComplex) {
|
|
191
|
+
const complexOptimization = this.optimizeComplexQuery(parsedQuery, originalQuery);
|
|
192
|
+
if (complexOptimization.improvement > improvement) {
|
|
193
|
+
optimizedQuery = complexOptimization.optimizedQuery;
|
|
194
|
+
improvement = complexOptimization.improvement;
|
|
195
|
+
type = 'query_rewrite';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
optimizedQuery,
|
|
200
|
+
type,
|
|
201
|
+
improvement
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Optimize SELECT queries
|
|
206
|
+
*/
|
|
207
|
+
optimizeSelectQuery(parsedQuery, originalQuery) {
|
|
208
|
+
let optimizedQuery = originalQuery;
|
|
209
|
+
let improvement = 0;
|
|
210
|
+
// Add LIMIT if missing and query could return many rows
|
|
211
|
+
if (!parsedQuery.hasLimit && query_parser_1.QueryParser.couldReturnManyRows(originalQuery)) {
|
|
212
|
+
optimizedQuery = `${originalQuery} LIMIT 1000`;
|
|
213
|
+
improvement = 10;
|
|
214
|
+
}
|
|
215
|
+
// Optimize ORDER BY with LIMIT
|
|
216
|
+
if (parsedQuery.hasLimit && parsedQuery.orderByColumns.length > 0) {
|
|
217
|
+
const orderByColumn = parsedQuery.orderByColumns[0];
|
|
218
|
+
const table = parsedQuery.tables[0];
|
|
219
|
+
if (table && this.hasIndexOnColumn(table, orderByColumn)) {
|
|
220
|
+
improvement = Math.max(improvement, 20);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
optimizedQuery,
|
|
225
|
+
improvement
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Optimize complex queries (joins, aggregates)
|
|
230
|
+
*/
|
|
231
|
+
optimizeComplexQuery(parsedQuery, originalQuery) {
|
|
232
|
+
let optimizedQuery = originalQuery;
|
|
233
|
+
let improvement = 0;
|
|
234
|
+
// Check for missing indexes on join columns
|
|
235
|
+
for (const { table, column } of parsedQuery.joinColumns) {
|
|
236
|
+
if (!this.hasIndexOnColumn(table, column)) {
|
|
237
|
+
improvement = Math.max(improvement, 15);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Check for missing indexes on WHERE columns
|
|
241
|
+
for (const column of parsedQuery.whereColumns) {
|
|
242
|
+
const table = parsedQuery.tables[0];
|
|
243
|
+
if (table && !this.hasIndexOnColumn(table, column)) {
|
|
244
|
+
improvement = Math.max(improvement, 20);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Check for missing indexes on GROUP BY columns
|
|
248
|
+
if (parsedQuery.hasAggregates && parsedQuery.groupByColumns.length > 0) {
|
|
249
|
+
const table = parsedQuery.tables[0];
|
|
250
|
+
if (table && parsedQuery.groupByColumns.every(col => this.hasIndexOnColumn(table, col))) {
|
|
251
|
+
improvement = Math.max(improvement, 25);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
optimizedQuery,
|
|
256
|
+
improvement
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Analyze query for index recommendations
|
|
261
|
+
*/
|
|
262
|
+
analyzeQueryForIndexes(parsedQuery, originalQuery) {
|
|
263
|
+
const recommendations = [];
|
|
264
|
+
const table = parsedQuery.tables[0];
|
|
265
|
+
if (!table) {
|
|
266
|
+
return recommendations;
|
|
267
|
+
}
|
|
268
|
+
// Analyze WHERE clauses
|
|
269
|
+
for (const column of parsedQuery.whereColumns) {
|
|
270
|
+
if (!this.hasIndexOnColumn(table, column)) {
|
|
271
|
+
recommendations.push({
|
|
272
|
+
table,
|
|
273
|
+
columns: [column],
|
|
274
|
+
type: 'btree',
|
|
275
|
+
reason: `WHERE clause on ${column}`,
|
|
276
|
+
estimatedImprovement: 20,
|
|
277
|
+
priority: 'medium'
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Analyze JOIN columns
|
|
282
|
+
for (const { table: joinTable, column } of parsedQuery.joinColumns) {
|
|
283
|
+
if (!this.hasIndexOnColumn(joinTable, column)) {
|
|
284
|
+
recommendations.push({
|
|
285
|
+
table: joinTable,
|
|
286
|
+
columns: [column],
|
|
287
|
+
type: 'btree',
|
|
288
|
+
reason: `JOIN condition on ${column}`,
|
|
289
|
+
estimatedImprovement: 30,
|
|
290
|
+
priority: 'high'
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Analyze ORDER BY columns
|
|
295
|
+
for (const column of parsedQuery.orderByColumns) {
|
|
296
|
+
if (!this.hasIndexOnColumn(table, column)) {
|
|
297
|
+
recommendations.push({
|
|
298
|
+
table,
|
|
299
|
+
columns: [column],
|
|
300
|
+
type: 'btree',
|
|
301
|
+
reason: `ORDER BY clause on ${column}`,
|
|
302
|
+
estimatedImprovement: 15,
|
|
303
|
+
priority: 'medium'
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return recommendations;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Deduplicate index recommendations
|
|
311
|
+
*/
|
|
312
|
+
deduplicateRecommendations(recommendations) {
|
|
313
|
+
const unique = new Map();
|
|
314
|
+
for (const rec of recommendations) {
|
|
315
|
+
const key = `${rec.table}:${rec.columns.join(',')}`;
|
|
316
|
+
if (!unique.has(key) || unique.get(key).priority === 'low') {
|
|
317
|
+
unique.set(key, rec);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return Array.from(unique.values()).sort((a, b) => {
|
|
321
|
+
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|
322
|
+
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Generate optimization suggestions
|
|
327
|
+
*/
|
|
328
|
+
generateSuggestions(parsedQuery, executionTime, context) {
|
|
329
|
+
const suggestions = [];
|
|
330
|
+
if (executionTime > this.options.slowQueryThreshold) {
|
|
331
|
+
suggestions.push(`Query took ${executionTime.toFixed(2)}ms - consider adding indexes`);
|
|
332
|
+
}
|
|
333
|
+
if (parsedQuery.type === 'SELECT' && !parsedQuery.hasLimit) {
|
|
334
|
+
suggestions.push('Consider adding LIMIT clause to prevent large result sets');
|
|
335
|
+
}
|
|
336
|
+
if (parsedQuery.joinColumns.length > 0) {
|
|
337
|
+
for (const { table, column } of parsedQuery.joinColumns) {
|
|
338
|
+
if (!this.hasIndexOnColumn(table, column)) {
|
|
339
|
+
suggestions.push(`Consider adding index on ${table}.${column} for join optimization`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (parsedQuery.hasAggregates && parsedQuery.groupByColumns.length > 0) {
|
|
344
|
+
suggestions.push('Consider adding composite indexes for GROUP BY columns');
|
|
345
|
+
}
|
|
346
|
+
return suggestions;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Execute a query with error handling
|
|
350
|
+
* Note: This creates a proper SQL query with parameters
|
|
351
|
+
*/
|
|
352
|
+
async executeQuery(query, params) {
|
|
353
|
+
try {
|
|
354
|
+
// Split query by '?' placeholders and create template string array
|
|
355
|
+
const parts = query.split('?');
|
|
356
|
+
const templateStrings = Object.assign([...parts], { raw: [...parts] });
|
|
357
|
+
// Create SQL query using template tag syntax
|
|
358
|
+
const sqlQuery = (0, sql_1.sql)(templateStrings, ...params);
|
|
359
|
+
// Compile the query first to match the expected interface
|
|
360
|
+
const compiledQuery = sqlQuery.compile(this.db);
|
|
361
|
+
const result = await this.db.executeQuery(compiledQuery);
|
|
362
|
+
return result.rows;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
366
|
+
this.logger.error(`Query execution failed: ${errorMessage}`, { query, params });
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Check if column has an index
|
|
372
|
+
*/
|
|
373
|
+
hasIndexOnColumn(table, column) {
|
|
374
|
+
const tableInfo = this.schemaInfo.tables.find(t => t.name === table);
|
|
375
|
+
if (!tableInfo)
|
|
376
|
+
return false;
|
|
377
|
+
// Check if column is primary key
|
|
378
|
+
const columnInfo = tableInfo.columns.find(col => col.name === column);
|
|
379
|
+
if (columnInfo?.isPrimaryKey)
|
|
380
|
+
return true;
|
|
381
|
+
// Check if column has an index
|
|
382
|
+
return tableInfo.indexes.some(idx => idx.columns.includes(column));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
exports.QueryOptimizer = QueryOptimizer;
|
|
386
|
+
/**
|
|
387
|
+
* Factory function to create query optimizer
|
|
388
|
+
*/
|
|
389
|
+
function createQueryOptimizer(db, schemaInfo, options, logger) {
|
|
390
|
+
return new QueryOptimizer(db, schemaInfo, options, logger);
|
|
391
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Logger } from '../../logging/logger.js';
|
|
2
|
+
export interface CacheEntry<T = any> {
|
|
3
|
+
key: string;
|
|
4
|
+
value: T;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
ttl: number;
|
|
7
|
+
hitCount: number;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CacheConfig {
|
|
11
|
+
maxSize: number;
|
|
12
|
+
defaultTtl: number;
|
|
13
|
+
cleanupInterval: number;
|
|
14
|
+
enableCompression: boolean;
|
|
15
|
+
enableMetrics: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface CacheStats {
|
|
18
|
+
size: number;
|
|
19
|
+
maxSize: number;
|
|
20
|
+
hitCount: number;
|
|
21
|
+
missCount: number;
|
|
22
|
+
hitRate: number;
|
|
23
|
+
totalSize: number;
|
|
24
|
+
evictions: number;
|
|
25
|
+
lastCleanup: Date;
|
|
26
|
+
}
|
|
27
|
+
export interface CacheMetrics {
|
|
28
|
+
operations: {
|
|
29
|
+
get: number;
|
|
30
|
+
set: number;
|
|
31
|
+
delete: number;
|
|
32
|
+
clear: number;
|
|
33
|
+
};
|
|
34
|
+
performance: {
|
|
35
|
+
averageGetTime: number;
|
|
36
|
+
averageSetTime: number;
|
|
37
|
+
slowestOperation: number;
|
|
38
|
+
};
|
|
39
|
+
memory: {
|
|
40
|
+
totalSize: number;
|
|
41
|
+
averageEntrySize: number;
|
|
42
|
+
largestEntry: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generic cache service with TTL, size limits, and metrics
|
|
47
|
+
*/
|
|
48
|
+
export declare class CacheService<T = any> {
|
|
49
|
+
private cache;
|
|
50
|
+
private config;
|
|
51
|
+
private stats;
|
|
52
|
+
private metrics;
|
|
53
|
+
private cleanupTimer?;
|
|
54
|
+
protected logger: Logger;
|
|
55
|
+
constructor(config?: Partial<CacheConfig>, logger?: Logger);
|
|
56
|
+
/**
|
|
57
|
+
* Get a value from cache
|
|
58
|
+
*/
|
|
59
|
+
get(key: string): T | null;
|
|
60
|
+
/**
|
|
61
|
+
* Set a value in cache
|
|
62
|
+
*/
|
|
63
|
+
set(key: string, value: T, ttl?: number): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Delete a value from cache
|
|
66
|
+
*/
|
|
67
|
+
delete(key: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Check if key exists in cache
|
|
70
|
+
*/
|
|
71
|
+
has(key: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Clear all entries from cache
|
|
74
|
+
*/
|
|
75
|
+
clear(): void;
|
|
76
|
+
/**
|
|
77
|
+
* Get cache statistics
|
|
78
|
+
*/
|
|
79
|
+
getStats(): CacheStats;
|
|
80
|
+
/**
|
|
81
|
+
* Get detailed metrics
|
|
82
|
+
*/
|
|
83
|
+
getMetrics(): CacheMetrics;
|
|
84
|
+
/**
|
|
85
|
+
* Get cache size
|
|
86
|
+
*/
|
|
87
|
+
size(): number;
|
|
88
|
+
/**
|
|
89
|
+
* Get all cache keys
|
|
90
|
+
*/
|
|
91
|
+
keys(): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Get cache entries (for debugging)
|
|
94
|
+
*/
|
|
95
|
+
entries(): Array<{
|
|
96
|
+
key: string;
|
|
97
|
+
entry: CacheEntry<T>;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Warm cache with multiple entries
|
|
101
|
+
*/
|
|
102
|
+
warm(entries: Array<{
|
|
103
|
+
key: string;
|
|
104
|
+
value: T;
|
|
105
|
+
ttl?: number;
|
|
106
|
+
}>): number;
|
|
107
|
+
/**
|
|
108
|
+
* Get cache health status
|
|
109
|
+
*/
|
|
110
|
+
getHealth(): {
|
|
111
|
+
status: 'healthy' | 'warning' | 'critical';
|
|
112
|
+
issues: string[];
|
|
113
|
+
recommendations: string[];
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Shutdown cache service
|
|
117
|
+
*/
|
|
118
|
+
shutdown(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Check if cache entry is expired
|
|
121
|
+
*/
|
|
122
|
+
private isExpired;
|
|
123
|
+
/**
|
|
124
|
+
* Calculate size of a value (rough estimation)
|
|
125
|
+
*/
|
|
126
|
+
private calculateSize;
|
|
127
|
+
/**
|
|
128
|
+
* Evict entries when cache is full
|
|
129
|
+
*/
|
|
130
|
+
private evictEntries;
|
|
131
|
+
/**
|
|
132
|
+
* Update cache statistics
|
|
133
|
+
*/
|
|
134
|
+
private updateStats;
|
|
135
|
+
/**
|
|
136
|
+
* Update hit rate
|
|
137
|
+
*/
|
|
138
|
+
private updateHitRate;
|
|
139
|
+
/**
|
|
140
|
+
* Update performance metrics
|
|
141
|
+
*/
|
|
142
|
+
private updatePerformanceMetrics;
|
|
143
|
+
/**
|
|
144
|
+
* Start cleanup timer
|
|
145
|
+
*/
|
|
146
|
+
private startCleanupTimer;
|
|
147
|
+
/**
|
|
148
|
+
* Cleanup expired entries
|
|
149
|
+
*/
|
|
150
|
+
private cleanup;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Factory function to create cache service
|
|
154
|
+
*/
|
|
155
|
+
export declare function createCacheService<T = any>(config?: Partial<CacheConfig>, logger?: Logger): CacheService<T>;
|
|
156
|
+
/**
|
|
157
|
+
* Specialized query cache service
|
|
158
|
+
*/
|
|
159
|
+
export declare class QueryCacheService extends CacheService<any> {
|
|
160
|
+
constructor(config?: Partial<CacheConfig>, logger?: Logger);
|
|
161
|
+
/**
|
|
162
|
+
* Cache query result with automatic key generation
|
|
163
|
+
*/
|
|
164
|
+
cacheQuery(query: string, params: any[], result: any, ttl?: number): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Get cached query result
|
|
167
|
+
*/
|
|
168
|
+
getCachedQuery(query: string, params: any[]): any | null;
|
|
169
|
+
/**
|
|
170
|
+
* Generate cache key for query
|
|
171
|
+
*/
|
|
172
|
+
private generateQueryKey;
|
|
173
|
+
/**
|
|
174
|
+
* Invalidate cache entries for a specific table
|
|
175
|
+
*/
|
|
176
|
+
invalidateTable(table: string): number;
|
|
177
|
+
}
|