@framers/sql-storage-adapter 0.3.5 → 0.4.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/README.md +94 -1
- package/dist/adapters/baseStorageAdapter.d.ts +172 -8
- package/dist/adapters/baseStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/baseStorageAdapter.js +487 -21
- package/dist/adapters/baseStorageAdapter.js.map +1 -1
- package/dist/adapters/betterSqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/betterSqliteAdapter.js +8 -0
- package/dist/adapters/betterSqliteAdapter.js.map +1 -1
- package/dist/core/contracts/hooks.d.ts +481 -0
- package/dist/core/contracts/hooks.d.ts.map +1 -0
- package/dist/core/contracts/hooks.js +215 -0
- package/dist/core/contracts/hooks.js.map +1 -0
- package/dist/core/contracts/index.d.ts +10 -0
- package/dist/core/contracts/index.d.ts.map +1 -1
- package/dist/core/contracts/index.js +13 -0
- package/dist/core/contracts/index.js.map +1 -1
- package/dist/core/contracts/performance.d.ts +308 -0
- package/dist/core/contracts/performance.d.ts.map +1 -0
- package/dist/core/contracts/performance.js +186 -0
- package/dist/core/contracts/performance.js.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* - Parameter validation and sanitization
|
|
10
10
|
* - Error handling and standardization
|
|
11
11
|
* - Lifecycle management (open/close state tracking)
|
|
12
|
-
* - Performance monitoring
|
|
12
|
+
* - Performance monitoring with configurable tiers
|
|
13
|
+
* - Query result caching (optional)
|
|
14
|
+
* - Lifecycle hooks for RAG integration
|
|
13
15
|
* - Logging and diagnostics
|
|
14
16
|
*
|
|
15
17
|
* @example Implementing a new adapter
|
|
@@ -26,7 +28,23 @@
|
|
|
26
28
|
* // ... implement other abstract methods
|
|
27
29
|
* }
|
|
28
30
|
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example Using with performance tiers and hooks
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const adapter = new MyAdapter({
|
|
35
|
+
* performance: { tier: 'balanced', trackMetrics: true },
|
|
36
|
+
* hooks: {
|
|
37
|
+
* onBeforeWrite: async (ctx) => {
|
|
38
|
+
* // Generate embedding for RAG
|
|
39
|
+
* ctx.metadata = { embedding: await embed(ctx.parameters?.[0]) };
|
|
40
|
+
* return ctx;
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
29
45
|
*/
|
|
46
|
+
import { resolvePerformanceConfig, isTransientError, } from '../core/contracts/performance.js';
|
|
47
|
+
import { generateOperationId } from '../core/contracts/hooks.js';
|
|
30
48
|
/**
|
|
31
49
|
* Base state for all adapters.
|
|
32
50
|
*/
|
|
@@ -45,7 +63,9 @@ var AdapterState;
|
|
|
45
63
|
* - State management (open/close tracking)
|
|
46
64
|
* - Parameter validation
|
|
47
65
|
* - Error handling and wrapping
|
|
48
|
-
* - Performance metrics
|
|
66
|
+
* - Performance metrics with configurable tiers
|
|
67
|
+
* - Query result caching (tier-dependent)
|
|
68
|
+
* - Lifecycle hooks for RAG and analytics
|
|
49
69
|
* - Logging and diagnostics
|
|
50
70
|
*/
|
|
51
71
|
export class BaseStorageAdapter {
|
|
@@ -53,6 +73,31 @@ export class BaseStorageAdapter {
|
|
|
53
73
|
* Creates a new adapter instance.
|
|
54
74
|
*
|
|
55
75
|
* @param options - Configuration options for the adapter
|
|
76
|
+
*
|
|
77
|
+
* @example Default balanced tier
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const adapter = new MyAdapter();
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example Fast tier for development
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const adapter = new MyAdapter({
|
|
85
|
+
* performance: { tier: 'fast' },
|
|
86
|
+
* verbose: true
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example With RAG hooks
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const adapter = new MyAdapter({
|
|
93
|
+
* hooks: {
|
|
94
|
+
* onBeforeWrite: async (ctx) => {
|
|
95
|
+
* ctx.metadata = { embedding: await embed(ctx.parameters) };
|
|
96
|
+
* return ctx;
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
56
101
|
*/
|
|
57
102
|
constructor(options = {}) {
|
|
58
103
|
// State management
|
|
@@ -64,17 +109,39 @@ export class BaseStorageAdapter {
|
|
|
64
109
|
totalTransactions: 0,
|
|
65
110
|
totalErrors: 0,
|
|
66
111
|
averageQueryDuration: 0,
|
|
67
|
-
openedAt: null
|
|
112
|
+
openedAt: null,
|
|
113
|
+
slowQueries: 0,
|
|
114
|
+
retries: 0,
|
|
115
|
+
};
|
|
116
|
+
// Cache
|
|
117
|
+
this.queryCache = new Map();
|
|
118
|
+
this.cacheStats = {
|
|
119
|
+
hits: 0,
|
|
120
|
+
misses: 0,
|
|
121
|
+
hitRatio: 0,
|
|
122
|
+
size: 0,
|
|
123
|
+
bytesUsed: 0,
|
|
124
|
+
evictions: 0,
|
|
125
|
+
invalidations: 0,
|
|
68
126
|
};
|
|
69
127
|
// Performance tracking
|
|
70
128
|
this.queryDurations = [];
|
|
71
129
|
this.MAX_DURATION_SAMPLES = 100; // Keep last 100 for rolling average
|
|
72
|
-
this.options =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
this.options = options;
|
|
131
|
+
// Resolve performance config, allowing top-level validateSQL to override
|
|
132
|
+
const perfConfig = options.performance ?? {};
|
|
133
|
+
const resolvedSettings = resolvePerformanceConfig(perfConfig);
|
|
134
|
+
// Allow top-level validateSQL to override performance.validateSql
|
|
135
|
+
if (options.validateSQL !== undefined) {
|
|
136
|
+
this.performanceSettings = {
|
|
137
|
+
...resolvedSettings,
|
|
138
|
+
validateSql: options.validateSQL,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.performanceSettings = resolvedSettings;
|
|
143
|
+
}
|
|
144
|
+
this.hooks = options.hooks ?? {};
|
|
78
145
|
}
|
|
79
146
|
// ============================================================================
|
|
80
147
|
// Public Interface (Template Methods)
|
|
@@ -106,59 +173,205 @@ export class BaseStorageAdapter {
|
|
|
106
173
|
}
|
|
107
174
|
/**
|
|
108
175
|
* Executes a mutation statement (INSERT, UPDATE, DELETE).
|
|
176
|
+
*
|
|
177
|
+
* Invokes `onBeforeWrite` and `onAfterWrite` hooks if configured.
|
|
178
|
+
* Supports retry on transient errors based on performance tier.
|
|
109
179
|
*/
|
|
110
180
|
async run(statement, parameters) {
|
|
111
181
|
this.assertOpen();
|
|
112
182
|
this.validateStatement(statement);
|
|
113
183
|
const startTime = Date.now();
|
|
184
|
+
const operationId = generateOperationId();
|
|
185
|
+
// Build write context for hooks
|
|
186
|
+
let context = {
|
|
187
|
+
operationId,
|
|
188
|
+
operation: 'run',
|
|
189
|
+
startTime,
|
|
190
|
+
adapterKind: this.kind,
|
|
191
|
+
statement,
|
|
192
|
+
parameters,
|
|
193
|
+
affectedTables: this.extractTables(statement),
|
|
194
|
+
};
|
|
114
195
|
try {
|
|
115
|
-
|
|
196
|
+
// Execute onBeforeWrite hook
|
|
197
|
+
if (this.hooks.onBeforeWrite) {
|
|
198
|
+
const hookResult = await this.hooks.onBeforeWrite(context);
|
|
199
|
+
if (hookResult === undefined) {
|
|
200
|
+
// Hook aborted the operation
|
|
201
|
+
return { changes: 0 };
|
|
202
|
+
}
|
|
203
|
+
context = hookResult;
|
|
204
|
+
}
|
|
205
|
+
// Execute with retry logic
|
|
206
|
+
const result = await this.executeWithRetry(() => this.performRun(context.statement, context.parameters), context);
|
|
207
|
+
const duration = Date.now() - startTime;
|
|
116
208
|
this.metrics.totalMutations++;
|
|
117
|
-
this.trackDuration(
|
|
209
|
+
this.trackDuration(duration);
|
|
210
|
+
this.checkSlowQuery(duration, context.statement);
|
|
211
|
+
// Invalidate cache for affected tables
|
|
212
|
+
if (this.performanceSettings.cacheEnabled && context.affectedTables) {
|
|
213
|
+
this.invalidateCache(context.affectedTables);
|
|
214
|
+
}
|
|
118
215
|
this.log(`Mutation executed: ${statement.substring(0, 50)}... (${result.changes} rows affected)`);
|
|
216
|
+
// Execute onAfterWrite hook
|
|
217
|
+
if (this.hooks.onAfterWrite) {
|
|
218
|
+
await this.hooks.onAfterWrite(context, result);
|
|
219
|
+
}
|
|
119
220
|
return result;
|
|
120
221
|
}
|
|
121
222
|
catch (error) {
|
|
122
223
|
this.metrics.totalErrors++;
|
|
123
|
-
|
|
224
|
+
const wrappedError = this.wrapError(`Failed to execute mutation: ${statement}`, error);
|
|
225
|
+
// Execute onError hook
|
|
226
|
+
if (this.hooks.onError) {
|
|
227
|
+
const hookResult = await this.hooks.onError(wrappedError, context);
|
|
228
|
+
if (hookResult === undefined) {
|
|
229
|
+
// Hook suppressed the error
|
|
230
|
+
return { changes: 0 };
|
|
231
|
+
}
|
|
232
|
+
throw hookResult;
|
|
233
|
+
}
|
|
234
|
+
throw wrappedError;
|
|
124
235
|
}
|
|
125
236
|
}
|
|
126
237
|
/**
|
|
127
238
|
* Retrieves a single row.
|
|
239
|
+
*
|
|
240
|
+
* Supports caching based on performance tier and invokes query hooks.
|
|
128
241
|
*/
|
|
129
242
|
async get(statement, parameters) {
|
|
130
243
|
this.assertOpen();
|
|
131
244
|
this.validateStatement(statement);
|
|
132
245
|
const startTime = Date.now();
|
|
246
|
+
const operationId = generateOperationId();
|
|
247
|
+
const cacheKey = this.getCacheKey('get', statement, parameters);
|
|
248
|
+
// Check cache first
|
|
249
|
+
if (this.performanceSettings.cacheEnabled) {
|
|
250
|
+
const cached = this.getFromCache(cacheKey);
|
|
251
|
+
if (cached !== undefined) {
|
|
252
|
+
return cached;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Build query context for hooks
|
|
256
|
+
let context = {
|
|
257
|
+
operationId,
|
|
258
|
+
operation: 'get',
|
|
259
|
+
startTime,
|
|
260
|
+
adapterKind: this.kind,
|
|
261
|
+
statement,
|
|
262
|
+
parameters,
|
|
263
|
+
affectedTables: this.extractTables(statement),
|
|
264
|
+
};
|
|
133
265
|
try {
|
|
134
|
-
|
|
266
|
+
// Execute onBeforeQuery hook
|
|
267
|
+
if (this.hooks.onBeforeQuery) {
|
|
268
|
+
const hookResult = await this.hooks.onBeforeQuery(context);
|
|
269
|
+
if (hookResult === undefined) {
|
|
270
|
+
return null; // Hook aborted
|
|
271
|
+
}
|
|
272
|
+
context = hookResult;
|
|
273
|
+
}
|
|
274
|
+
// Execute with retry
|
|
275
|
+
let result = await this.executeWithRetry(() => this.performGet(context.statement, context.parameters), context);
|
|
276
|
+
const duration = Date.now() - startTime;
|
|
135
277
|
this.metrics.totalQueries++;
|
|
136
|
-
this.trackDuration(
|
|
278
|
+
this.trackDuration(duration);
|
|
279
|
+
this.checkSlowQuery(duration, context.statement);
|
|
137
280
|
this.log(`Query executed: ${statement.substring(0, 50)}... (${result ? '1 row' : 'no rows'})`);
|
|
281
|
+
// Execute onAfterQuery hook
|
|
282
|
+
if (this.hooks.onAfterQuery) {
|
|
283
|
+
const hookResult = await this.hooks.onAfterQuery(context, result);
|
|
284
|
+
if (hookResult !== undefined) {
|
|
285
|
+
result = hookResult;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Cache the result
|
|
289
|
+
if (this.performanceSettings.cacheEnabled) {
|
|
290
|
+
this.setCache(cacheKey, result, context.affectedTables);
|
|
291
|
+
}
|
|
138
292
|
return result;
|
|
139
293
|
}
|
|
140
294
|
catch (error) {
|
|
141
295
|
this.metrics.totalErrors++;
|
|
142
|
-
|
|
296
|
+
const wrappedError = this.wrapError(`Failed to execute query: ${statement}`, error);
|
|
297
|
+
if (this.hooks.onError) {
|
|
298
|
+
const hookResult = await this.hooks.onError(wrappedError, context);
|
|
299
|
+
if (hookResult === undefined) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
throw hookResult;
|
|
303
|
+
}
|
|
304
|
+
throw wrappedError;
|
|
143
305
|
}
|
|
144
306
|
}
|
|
145
307
|
/**
|
|
146
308
|
* Retrieves all rows.
|
|
309
|
+
*
|
|
310
|
+
* Supports caching based on performance tier and invokes query hooks.
|
|
147
311
|
*/
|
|
148
312
|
async all(statement, parameters) {
|
|
149
313
|
this.assertOpen();
|
|
150
314
|
this.validateStatement(statement);
|
|
151
315
|
const startTime = Date.now();
|
|
316
|
+
const operationId = generateOperationId();
|
|
317
|
+
const cacheKey = this.getCacheKey('all', statement, parameters);
|
|
318
|
+
// Check cache first
|
|
319
|
+
if (this.performanceSettings.cacheEnabled) {
|
|
320
|
+
const cached = this.getFromCache(cacheKey);
|
|
321
|
+
if (cached !== undefined) {
|
|
322
|
+
return cached;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Build query context for hooks
|
|
326
|
+
let context = {
|
|
327
|
+
operationId,
|
|
328
|
+
operation: 'all',
|
|
329
|
+
startTime,
|
|
330
|
+
adapterKind: this.kind,
|
|
331
|
+
statement,
|
|
332
|
+
parameters,
|
|
333
|
+
affectedTables: this.extractTables(statement),
|
|
334
|
+
};
|
|
152
335
|
try {
|
|
153
|
-
|
|
336
|
+
// Execute onBeforeQuery hook
|
|
337
|
+
if (this.hooks.onBeforeQuery) {
|
|
338
|
+
const hookResult = await this.hooks.onBeforeQuery(context);
|
|
339
|
+
if (hookResult === undefined) {
|
|
340
|
+
return []; // Hook aborted
|
|
341
|
+
}
|
|
342
|
+
context = hookResult;
|
|
343
|
+
}
|
|
344
|
+
// Execute with retry
|
|
345
|
+
let results = await this.executeWithRetry(() => this.performAll(context.statement, context.parameters), context);
|
|
346
|
+
const duration = Date.now() - startTime;
|
|
154
347
|
this.metrics.totalQueries++;
|
|
155
|
-
this.trackDuration(
|
|
348
|
+
this.trackDuration(duration);
|
|
349
|
+
this.checkSlowQuery(duration, context.statement);
|
|
156
350
|
this.log(`Query executed: ${statement.substring(0, 50)}... (${results.length} rows)`);
|
|
351
|
+
// Execute onAfterQuery hook
|
|
352
|
+
if (this.hooks.onAfterQuery) {
|
|
353
|
+
const hookResult = await this.hooks.onAfterQuery(context, results);
|
|
354
|
+
if (hookResult !== undefined) {
|
|
355
|
+
results = hookResult;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Cache the result
|
|
359
|
+
if (this.performanceSettings.cacheEnabled) {
|
|
360
|
+
this.setCache(cacheKey, results, context.affectedTables);
|
|
361
|
+
}
|
|
157
362
|
return results;
|
|
158
363
|
}
|
|
159
364
|
catch (error) {
|
|
160
365
|
this.metrics.totalErrors++;
|
|
161
|
-
|
|
366
|
+
const wrappedError = this.wrapError(`Failed to execute query: ${statement}`, error);
|
|
367
|
+
if (this.hooks.onError) {
|
|
368
|
+
const hookResult = await this.hooks.onError(wrappedError, context);
|
|
369
|
+
if (hookResult === undefined) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
throw hookResult;
|
|
373
|
+
}
|
|
374
|
+
throw wrappedError;
|
|
162
375
|
}
|
|
163
376
|
}
|
|
164
377
|
/**
|
|
@@ -182,20 +395,60 @@ export class BaseStorageAdapter {
|
|
|
182
395
|
}
|
|
183
396
|
/**
|
|
184
397
|
* Executes a transaction.
|
|
398
|
+
*
|
|
399
|
+
* Invokes transaction hooks and invalidates cache on completion.
|
|
185
400
|
*/
|
|
186
401
|
async transaction(fn) {
|
|
187
402
|
this.assertOpen();
|
|
188
403
|
const startTime = Date.now();
|
|
404
|
+
const operationId = generateOperationId();
|
|
405
|
+
// Build transaction context
|
|
406
|
+
let context = {
|
|
407
|
+
operationId,
|
|
408
|
+
operation: 'transaction',
|
|
409
|
+
startTime,
|
|
410
|
+
adapterKind: this.kind,
|
|
411
|
+
};
|
|
189
412
|
try {
|
|
413
|
+
// Execute onBeforeTransaction hook
|
|
414
|
+
if (this.hooks.onBeforeTransaction) {
|
|
415
|
+
const hookResult = await this.hooks.onBeforeTransaction(context);
|
|
416
|
+
if (hookResult === undefined) {
|
|
417
|
+
throw new Error('Transaction aborted by hook');
|
|
418
|
+
}
|
|
419
|
+
context = hookResult;
|
|
420
|
+
}
|
|
190
421
|
const result = await this.performTransaction(fn);
|
|
422
|
+
context.outcome = 'committed';
|
|
191
423
|
this.metrics.totalTransactions++;
|
|
192
424
|
this.trackDuration(Date.now() - startTime);
|
|
193
425
|
this.log('Transaction committed successfully');
|
|
426
|
+
// Invalidate all cache on transaction commit (conservative approach)
|
|
427
|
+
if (this.performanceSettings.cacheEnabled) {
|
|
428
|
+
this.clearCache();
|
|
429
|
+
}
|
|
430
|
+
// Execute onAfterTransaction hook
|
|
431
|
+
if (this.hooks.onAfterTransaction) {
|
|
432
|
+
await this.hooks.onAfterTransaction(context);
|
|
433
|
+
}
|
|
194
434
|
return result;
|
|
195
435
|
}
|
|
196
436
|
catch (error) {
|
|
437
|
+
context.outcome = 'rolled_back';
|
|
197
438
|
this.metrics.totalErrors++;
|
|
198
|
-
|
|
439
|
+
const wrappedError = this.wrapError('Transaction failed', error);
|
|
440
|
+
// Execute hooks
|
|
441
|
+
if (this.hooks.onAfterTransaction) {
|
|
442
|
+
await this.hooks.onAfterTransaction(context);
|
|
443
|
+
}
|
|
444
|
+
if (this.hooks.onError) {
|
|
445
|
+
const hookResult = await this.hooks.onError(wrappedError, context);
|
|
446
|
+
if (hookResult === undefined) {
|
|
447
|
+
throw new Error('Transaction rolled back');
|
|
448
|
+
}
|
|
449
|
+
throw hookResult;
|
|
450
|
+
}
|
|
451
|
+
throw wrappedError;
|
|
199
452
|
}
|
|
200
453
|
}
|
|
201
454
|
/**
|
|
@@ -277,7 +530,7 @@ export class BaseStorageAdapter {
|
|
|
277
530
|
* @throws {Error} If statement is invalid
|
|
278
531
|
*/
|
|
279
532
|
validateStatement(statement) {
|
|
280
|
-
if (!this.
|
|
533
|
+
if (!this.performanceSettings.validateSql) {
|
|
281
534
|
return;
|
|
282
535
|
}
|
|
283
536
|
if (!statement || !statement.trim()) {
|
|
@@ -288,6 +541,195 @@ export class BaseStorageAdapter {
|
|
|
288
541
|
this.log('Warning: SQL comment detected in statement');
|
|
289
542
|
}
|
|
290
543
|
}
|
|
544
|
+
// ============================================================================
|
|
545
|
+
// Cache Management
|
|
546
|
+
// ============================================================================
|
|
547
|
+
/**
|
|
548
|
+
* Generates a cache key for a query.
|
|
549
|
+
* Includes operation type to prevent get/all cache collisions.
|
|
550
|
+
*/
|
|
551
|
+
getCacheKey(operation, statement, parameters) {
|
|
552
|
+
const paramStr = parameters ? JSON.stringify(parameters) : '';
|
|
553
|
+
return `${operation}::${statement}::${paramStr}`;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Gets a value from cache if valid.
|
|
557
|
+
*/
|
|
558
|
+
getFromCache(key) {
|
|
559
|
+
const entry = this.queryCache.get(key);
|
|
560
|
+
if (!entry) {
|
|
561
|
+
this.cacheStats.misses++;
|
|
562
|
+
this.updateCacheHitRatio();
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
// Check expiration
|
|
566
|
+
if (Date.now() > entry.expiresAt) {
|
|
567
|
+
this.queryCache.delete(key);
|
|
568
|
+
this.cacheStats.misses++;
|
|
569
|
+
this.updateCacheHitRatio();
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
// Update LRU tracking
|
|
573
|
+
entry.hits++;
|
|
574
|
+
entry.lastAccessedAt = Date.now();
|
|
575
|
+
this.cacheStats.hits++;
|
|
576
|
+
this.updateCacheHitRatio();
|
|
577
|
+
return entry.data;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Sets a value in cache.
|
|
581
|
+
*/
|
|
582
|
+
setCache(key, data, affectedTables) {
|
|
583
|
+
// Enforce max entries
|
|
584
|
+
if (this.queryCache.size >= this.performanceSettings.cacheMaxEntries) {
|
|
585
|
+
this.evictLRU();
|
|
586
|
+
}
|
|
587
|
+
const now = Date.now();
|
|
588
|
+
const entry = {
|
|
589
|
+
data,
|
|
590
|
+
createdAt: now,
|
|
591
|
+
expiresAt: now + this.performanceSettings.cacheTtlMs,
|
|
592
|
+
affectedTables: affectedTables ?? [],
|
|
593
|
+
hits: 0,
|
|
594
|
+
lastAccessedAt: now,
|
|
595
|
+
};
|
|
596
|
+
this.queryCache.set(key, entry);
|
|
597
|
+
this.cacheStats.size = this.queryCache.size;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Invalidates cache entries for specific tables.
|
|
601
|
+
*/
|
|
602
|
+
invalidateCache(tables) {
|
|
603
|
+
const tableSet = new Set(tables.map(t => t.toLowerCase()));
|
|
604
|
+
for (const [key, entry] of this.queryCache.entries()) {
|
|
605
|
+
const hasAffectedTable = entry.affectedTables.some(t => tableSet.has(t.toLowerCase()));
|
|
606
|
+
if (hasAffectedTable) {
|
|
607
|
+
this.queryCache.delete(key);
|
|
608
|
+
this.cacheStats.invalidations++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
this.cacheStats.size = this.queryCache.size;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Clears entire cache.
|
|
615
|
+
*/
|
|
616
|
+
clearCache() {
|
|
617
|
+
this.queryCache.clear();
|
|
618
|
+
this.cacheStats.size = 0;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Evicts least recently used cache entry.
|
|
622
|
+
*/
|
|
623
|
+
evictLRU() {
|
|
624
|
+
let oldest = null;
|
|
625
|
+
for (const [key, entry] of this.queryCache.entries()) {
|
|
626
|
+
if (!oldest || entry.lastAccessedAt < oldest.time) {
|
|
627
|
+
oldest = { key, time: entry.lastAccessedAt };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (oldest) {
|
|
631
|
+
this.queryCache.delete(oldest.key);
|
|
632
|
+
this.cacheStats.evictions++;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Updates cache hit ratio.
|
|
637
|
+
*/
|
|
638
|
+
updateCacheHitRatio() {
|
|
639
|
+
const total = this.cacheStats.hits + this.cacheStats.misses;
|
|
640
|
+
this.cacheStats.hitRatio = total > 0 ? this.cacheStats.hits / total : 0;
|
|
641
|
+
}
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Retry Logic
|
|
644
|
+
// ============================================================================
|
|
645
|
+
/**
|
|
646
|
+
* Executes an operation with retry logic.
|
|
647
|
+
*/
|
|
648
|
+
async executeWithRetry(operation, context) {
|
|
649
|
+
if (!this.performanceSettings.retryOnError) {
|
|
650
|
+
return operation();
|
|
651
|
+
}
|
|
652
|
+
let lastError;
|
|
653
|
+
for (let attempt = 0; attempt <= this.performanceSettings.maxRetries; attempt++) {
|
|
654
|
+
try {
|
|
655
|
+
return await operation();
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
659
|
+
if (!isTransientError(lastError) || attempt >= this.performanceSettings.maxRetries) {
|
|
660
|
+
throw lastError;
|
|
661
|
+
}
|
|
662
|
+
this.metrics.retries++;
|
|
663
|
+
const delay = this.performanceSettings.retryDelayMs * Math.pow(2, attempt);
|
|
664
|
+
this.log(`Retrying operation ${context.operationId} in ${delay}ms (attempt ${attempt + 1})`);
|
|
665
|
+
await this.sleep(delay);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
throw lastError;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Sleep utility for retry delays.
|
|
672
|
+
*/
|
|
673
|
+
sleep(ms) {
|
|
674
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
675
|
+
}
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// SQL Parsing Utilities
|
|
678
|
+
// ============================================================================
|
|
679
|
+
/**
|
|
680
|
+
* Extracts table names from a SQL statement.
|
|
681
|
+
*
|
|
682
|
+
* @remarks
|
|
683
|
+
* Simple regex-based extraction. For complex queries, consider
|
|
684
|
+
* using a proper SQL parser.
|
|
685
|
+
*/
|
|
686
|
+
extractTables(statement) {
|
|
687
|
+
const tables = [];
|
|
688
|
+
const upperStatement = statement.toUpperCase();
|
|
689
|
+
// Match FROM table_name
|
|
690
|
+
const fromMatch = upperStatement.match(/FROM\s+([^\s,;(]+)/gi);
|
|
691
|
+
if (fromMatch) {
|
|
692
|
+
fromMatch.forEach(match => {
|
|
693
|
+
const table = match.replace(/^FROM\s+/i, '').trim();
|
|
694
|
+
if (table && !table.startsWith('(')) {
|
|
695
|
+
tables.push(table.toLowerCase());
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// Match INSERT INTO table_name
|
|
700
|
+
const insertMatch = statement.match(/INSERT\s+INTO\s+([^\s(]+)/i);
|
|
701
|
+
if (insertMatch?.[1]) {
|
|
702
|
+
tables.push(insertMatch[1].toLowerCase());
|
|
703
|
+
}
|
|
704
|
+
// Match UPDATE table_name
|
|
705
|
+
const updateMatch = statement.match(/UPDATE\s+([^\s]+)/i);
|
|
706
|
+
if (updateMatch?.[1]) {
|
|
707
|
+
tables.push(updateMatch[1].toLowerCase());
|
|
708
|
+
}
|
|
709
|
+
// Match DELETE FROM table_name
|
|
710
|
+
const deleteMatch = statement.match(/DELETE\s+FROM\s+([^\s]+)/i);
|
|
711
|
+
if (deleteMatch?.[1]) {
|
|
712
|
+
tables.push(deleteMatch[1].toLowerCase());
|
|
713
|
+
}
|
|
714
|
+
// Match JOIN table_name
|
|
715
|
+
const joinMatches = statement.match(/JOIN\s+([^\s]+)/gi);
|
|
716
|
+
if (joinMatches) {
|
|
717
|
+
joinMatches.forEach(match => {
|
|
718
|
+
const table = match.replace(/^JOIN\s+/i, '').trim();
|
|
719
|
+
tables.push(table.toLowerCase());
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return [...new Set(tables)]; // Deduplicate
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Checks if query duration exceeds slow query threshold.
|
|
726
|
+
*/
|
|
727
|
+
checkSlowQuery(duration, statement) {
|
|
728
|
+
if (duration > this.performanceSettings.slowQueryThresholdMs) {
|
|
729
|
+
this.metrics.slowQueries++;
|
|
730
|
+
this.log(`SLOW QUERY (${duration}ms): ${statement.substring(0, 100)}...`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
291
733
|
/**
|
|
292
734
|
* Wraps an error with adapter context.
|
|
293
735
|
*/
|
|
@@ -308,6 +750,12 @@ export class BaseStorageAdapter {
|
|
|
308
750
|
console.log(`[${this.kind}] ${message}`);
|
|
309
751
|
}
|
|
310
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Logs a warning message (always outputs regardless of verbose).
|
|
755
|
+
*/
|
|
756
|
+
logWarn(message) {
|
|
757
|
+
console.warn(`[${this.kind}] ${message}`);
|
|
758
|
+
}
|
|
311
759
|
/**
|
|
312
760
|
* Tracks query duration for performance metrics.
|
|
313
761
|
*/
|
|
@@ -337,7 +785,25 @@ export class BaseStorageAdapter {
|
|
|
337
785
|
* Gets performance metrics.
|
|
338
786
|
*/
|
|
339
787
|
getMetrics() {
|
|
340
|
-
return {
|
|
788
|
+
return {
|
|
789
|
+
...this.metrics,
|
|
790
|
+
cache: this.performanceSettings.cacheEnabled ? { ...this.cacheStats } : undefined,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Gets cache statistics.
|
|
795
|
+
*/
|
|
796
|
+
getCacheStats() {
|
|
797
|
+
if (!this.performanceSettings.cacheEnabled) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return { ...this.cacheStats };
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Gets current performance settings.
|
|
804
|
+
*/
|
|
805
|
+
getPerformanceSettings() {
|
|
806
|
+
return { ...this.performanceSettings };
|
|
341
807
|
}
|
|
342
808
|
/**
|
|
343
809
|
* Checks if adapter is open.
|