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.
Files changed (137) hide show
  1. package/LICENSE +89 -21
  2. package/README.md +195 -578
  3. package/dist/cjs/cli/commands/analyze.js +4 -1
  4. package/dist/cjs/cli/commands/generate.js +48 -8
  5. package/dist/cjs/cli/commands/init.js +54 -14
  6. package/dist/cjs/cli/commands/inspect.js +10 -3
  7. package/dist/cjs/cli/commands/migrate.js +38 -2
  8. package/dist/cjs/cli/commands/optimize.js +4 -1
  9. package/dist/cjs/cli/commands/status.js +41 -5
  10. package/dist/cjs/cli/commands/watch.js +4 -1
  11. package/dist/cjs/cli/index.js +4 -1
  12. package/dist/cjs/dialect/database-introspector.js +16 -21
  13. package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
  14. package/dist/cjs/dynamic/dynamic.d.ts +20 -0
  15. package/dist/cjs/dynamic/dynamic.js +25 -0
  16. package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
  17. package/dist/cjs/edge-runtime/edge-config.js +323 -0
  18. package/dist/cjs/errors/NoormError.d.ts +27 -4
  19. package/dist/cjs/errors/NoormError.js +142 -21
  20. package/dist/cjs/logging/logger.d.ts +7 -2
  21. package/dist/cjs/logging/logger.js +21 -7
  22. package/dist/cjs/noormme.d.ts +12 -8
  23. package/dist/cjs/noormme.js +133 -61
  24. package/dist/cjs/operation-node/column-node.js +4 -0
  25. package/dist/cjs/operation-node/identifier-node.js +4 -0
  26. package/dist/cjs/operation-node/table-node.js +7 -0
  27. package/dist/cjs/parser/reference-parser.js +5 -0
  28. package/dist/cjs/parser/table-parser.js +2 -0
  29. package/dist/cjs/performance/index.d.ts +44 -0
  30. package/dist/cjs/performance/index.js +64 -0
  31. package/dist/cjs/performance/query-optimizer.d.ts +134 -0
  32. package/dist/cjs/performance/query-optimizer.js +391 -0
  33. package/dist/cjs/performance/services/cache-service.d.ts +177 -0
  34. package/dist/cjs/performance/services/cache-service.js +415 -0
  35. package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
  36. package/dist/cjs/performance/services/connection-factory.js +498 -0
  37. package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
  38. package/dist/cjs/performance/services/metrics-collector.js +406 -0
  39. package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
  40. package/dist/cjs/performance/utils/query-parser.js +295 -0
  41. package/dist/cjs/raw-builder/sql.d.ts +73 -26
  42. package/dist/cjs/raw-builder/sql.js +9 -0
  43. package/dist/cjs/repository/repository-factory.d.ts +10 -42
  44. package/dist/cjs/repository/repository-factory.js +276 -394
  45. package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
  46. package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
  47. package/dist/cjs/schema/core/utils/name-generator.js +34 -2
  48. package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
  49. package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
  50. package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
  51. package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
  52. package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
  53. package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
  54. package/dist/cjs/schema/test/error-handling.test.js +52 -33
  55. package/dist/cjs/schema/test/integration.test.js +51 -5
  56. package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
  57. package/dist/cjs/sqlite-migration/index.js +35 -2
  58. package/dist/cjs/testing/test-utils.d.ts +5 -0
  59. package/dist/cjs/testing/test-utils.js +66 -6
  60. package/dist/cjs/types/index.d.ts +78 -13
  61. package/dist/cjs/types/index.js +23 -0
  62. package/dist/cjs/types/type-generator.d.ts +8 -0
  63. package/dist/cjs/types/type-generator.js +86 -17
  64. package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
  65. package/dist/cjs/util/safe-sql-helpers.js +221 -0
  66. package/dist/cjs/util/security-validator.d.ts +44 -0
  67. package/dist/cjs/util/security-validator.js +256 -0
  68. package/dist/cjs/util/security.d.ts +60 -0
  69. package/dist/cjs/util/security.js +137 -0
  70. package/dist/cjs/watch/schema-watcher.js +26 -7
  71. package/dist/esm/cli/commands/generate.js +10 -6
  72. package/dist/esm/cli/commands/init.js +15 -11
  73. package/dist/esm/cli/commands/inspect.js +6 -2
  74. package/dist/esm/cli/commands/status.js +3 -3
  75. package/dist/esm/dialect/database-introspector.js +16 -21
  76. package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
  77. package/dist/esm/dynamic/dynamic.d.ts +20 -0
  78. package/dist/esm/dynamic/dynamic.js +25 -0
  79. package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
  80. package/dist/esm/edge-runtime/edge-config.js +281 -0
  81. package/dist/esm/errors/NoormError.d.ts +27 -4
  82. package/dist/esm/errors/NoormError.js +134 -18
  83. package/dist/esm/logging/logger.d.ts +7 -2
  84. package/dist/esm/logging/logger.js +21 -7
  85. package/dist/esm/noormme.d.ts +12 -8
  86. package/dist/esm/noormme.js +130 -61
  87. package/dist/esm/operation-node/column-node.js +4 -0
  88. package/dist/esm/operation-node/identifier-node.js +4 -0
  89. package/dist/esm/operation-node/table-node.js +7 -0
  90. package/dist/esm/parser/reference-parser.js +5 -0
  91. package/dist/esm/parser/table-parser.js +2 -0
  92. package/dist/esm/performance/index.d.ts +44 -0
  93. package/dist/esm/performance/index.js +48 -0
  94. package/dist/esm/performance/query-optimizer.d.ts +134 -0
  95. package/dist/esm/performance/query-optimizer.js +387 -0
  96. package/dist/esm/performance/services/cache-service.d.ts +177 -0
  97. package/dist/esm/performance/services/cache-service.js +410 -0
  98. package/dist/esm/performance/services/connection-factory.d.ts +198 -0
  99. package/dist/esm/performance/services/connection-factory.js +493 -0
  100. package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
  101. package/dist/esm/performance/services/metrics-collector.js +402 -0
  102. package/dist/esm/performance/utils/query-parser.d.ts +123 -0
  103. package/dist/esm/performance/utils/query-parser.js +292 -0
  104. package/dist/esm/raw-builder/sql.d.ts +73 -26
  105. package/dist/esm/raw-builder/sql.js +9 -0
  106. package/dist/esm/repository/repository-factory.d.ts +10 -42
  107. package/dist/esm/repository/repository-factory.js +277 -395
  108. package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
  109. package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
  110. package/dist/esm/schema/core/utils/name-generator.js +34 -2
  111. package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
  112. package/dist/esm/schema/core/utils/type-mapper.js +4 -7
  113. package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
  114. package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
  115. package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
  116. package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
  117. package/dist/esm/schema/test/error-handling.test.js +52 -33
  118. package/dist/esm/schema/test/integration.test.js +18 -5
  119. package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
  120. package/dist/esm/testing/test-utils.d.ts +5 -0
  121. package/dist/esm/testing/test-utils.js +66 -6
  122. package/dist/esm/types/index.d.ts +78 -13
  123. package/dist/esm/types/index.js +22 -1
  124. package/dist/esm/types/type-generator.d.ts +8 -0
  125. package/dist/esm/types/type-generator.js +86 -17
  126. package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
  127. package/dist/esm/util/safe-sql-helpers.js +209 -0
  128. package/dist/esm/util/security-validator.d.ts +44 -0
  129. package/dist/esm/util/security-validator.js +246 -0
  130. package/dist/esm/util/security.d.ts +60 -0
  131. package/dist/esm/util/security.js +114 -0
  132. package/dist/esm/watch/schema-watcher.js +26 -7
  133. package/package.json +1 -1
  134. package/dist/cjs/performance/query-analyzer.d.ts +0 -89
  135. package/dist/cjs/performance/query-analyzer.js +0 -263
  136. package/dist/esm/performance/query-analyzer.d.ts +0 -89
  137. 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
- }