masterrecord 0.3.5 → 0.3.7
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/Cache/QueryCache.js +175 -0
- package/Cache/RedisQueryCache.js +155 -0
- package/QueryLanguage/queryMethods.js +72 -25
- package/context.js +51 -0
- package/package.json +1 -1
- package/readme.md +295 -4
- package/test/cacheIntegration.test.js +319 -0
- package/test/multiContextCache.test.js +230 -0
- package/test/multiContextCacheSimple.test.js +185 -0
- package/test/queryCache.test.js +148 -0
- package/examples/jsonArrayTransformer.js +0 -215
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Multi-Context Cache Sharing
|
|
3
|
+
*
|
|
4
|
+
* Verifies that cache is shared across context instances
|
|
5
|
+
* so invalidation in one context affects all contexts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const context = require('../context');
|
|
9
|
+
const QueryCache = require('../Cache/QueryCache');
|
|
10
|
+
|
|
11
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
12
|
+
console.log("║ Multi-Context Cache Sharing Test ║");
|
|
13
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
14
|
+
|
|
15
|
+
let passed = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
|
|
18
|
+
// Test 1: Multiple context instances share the same cache
|
|
19
|
+
console.log("📝 Test 1: Multiple contexts share same cache instance");
|
|
20
|
+
console.log("──────────────────────────────────────────────────");
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
class TestContext extends context {
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const db1 = new TestContext();
|
|
30
|
+
const db2 = new TestContext();
|
|
31
|
+
|
|
32
|
+
const areSameInstance = db1._queryCache === db2._queryCache;
|
|
33
|
+
const isSharedCache = db1._queryCache === context._sharedQueryCache;
|
|
34
|
+
|
|
35
|
+
if(areSameInstance && isSharedCache) {
|
|
36
|
+
console.log(" ✓ Both context instances share the same cache");
|
|
37
|
+
console.log(" ✓ Cache is stored in static property");
|
|
38
|
+
passed++;
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` ✗ Contexts have separate caches (BUG!)`);
|
|
41
|
+
console.log(` ✗ db1._queryCache === db2._queryCache: ${areSameInstance}`);
|
|
42
|
+
console.log(` ✗ Shared cache exists: ${isSharedCache}`);
|
|
43
|
+
failed++;
|
|
44
|
+
}
|
|
45
|
+
} catch(err) {
|
|
46
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
47
|
+
failed++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Test 2: Cache operations in one context affect another context
|
|
51
|
+
console.log("\n📝 Test 2: Cache operations are shared");
|
|
52
|
+
console.log("──────────────────────────────────────────────────");
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
class TestContext extends context {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const db1 = new TestContext();
|
|
62
|
+
const db2 = new TestContext();
|
|
63
|
+
|
|
64
|
+
// Context 1: Add to cache
|
|
65
|
+
const key = db1._queryCache.generateKey('SELECT * FROM users', [], 'users');
|
|
66
|
+
db1._queryCache.set(key, [{ id: 1, name: 'John' }], 'users');
|
|
67
|
+
|
|
68
|
+
// Context 2: Should see the same cached data
|
|
69
|
+
const cached = db2._queryCache.get(key);
|
|
70
|
+
|
|
71
|
+
if(cached && cached[0].name === 'John') {
|
|
72
|
+
console.log(" ✓ Cache data visible across contexts");
|
|
73
|
+
passed++;
|
|
74
|
+
} else {
|
|
75
|
+
console.log(` ✗ Cache data not shared across contexts`);
|
|
76
|
+
failed++;
|
|
77
|
+
}
|
|
78
|
+
} catch(err) {
|
|
79
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
80
|
+
failed++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Test 3: Cache invalidation in one context affects another
|
|
84
|
+
console.log("\n📝 Test 3: Cache invalidation is shared");
|
|
85
|
+
console.log("──────────────────────────────────────────────────");
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
class TestContext extends context {
|
|
89
|
+
constructor() {
|
|
90
|
+
super();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const db1 = new TestContext();
|
|
95
|
+
const db2 = new TestContext();
|
|
96
|
+
|
|
97
|
+
// Context 1: Add multiple entries to cache
|
|
98
|
+
const key1 = db1._queryCache.generateKey('SELECT * FROM users WHERE id=1', [], 'users');
|
|
99
|
+
const key2 = db1._queryCache.generateKey('SELECT * FROM users WHERE id=2', [], 'users');
|
|
100
|
+
db1._queryCache.set(key1, { id: 1, name: 'John' }, 'users');
|
|
101
|
+
db1._queryCache.set(key2, { id: 2, name: 'Jane' }, 'users');
|
|
102
|
+
|
|
103
|
+
// Verify both are cached
|
|
104
|
+
const beforeCached1 = db1._queryCache.get(key1);
|
|
105
|
+
const beforeCached2 = db1._queryCache.get(key2);
|
|
106
|
+
|
|
107
|
+
// Context 2: Invalidate User table
|
|
108
|
+
db2._queryCache.invalidateTable('users');
|
|
109
|
+
|
|
110
|
+
// Context 1: Should see invalidation
|
|
111
|
+
const afterCached1 = db1._queryCache.get(key1);
|
|
112
|
+
const afterCached2 = db1._queryCache.get(key2);
|
|
113
|
+
|
|
114
|
+
if(beforeCached1 !== null && beforeCached2 !== null && afterCached1 === null && afterCached2 === null) {
|
|
115
|
+
console.log(" ✓ Data was cached in context 1");
|
|
116
|
+
console.log(" ✓ Invalidation in context 2 affected context 1");
|
|
117
|
+
console.log(" ✓ Cache properly shared across contexts");
|
|
118
|
+
passed++;
|
|
119
|
+
} else {
|
|
120
|
+
console.log(` ✗ Invalidation not shared properly`);
|
|
121
|
+
console.log(` ✗ Before: cached1=${beforeCached1 !== null}, cached2=${beforeCached2 !== null}`);
|
|
122
|
+
console.log(` ✗ After: cached1=${afterCached1 !== null}, cached2=${afterCached2 !== null}`);
|
|
123
|
+
failed++;
|
|
124
|
+
}
|
|
125
|
+
} catch(err) {
|
|
126
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
127
|
+
failed++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Test 4: Cache statistics are shared
|
|
131
|
+
console.log("\n📝 Test 4: Cache statistics are shared");
|
|
132
|
+
console.log("──────────────────────────────────────────────────");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Clear existing cache for clean test
|
|
136
|
+
context._sharedQueryCache.clear();
|
|
137
|
+
|
|
138
|
+
class TestContext extends context {
|
|
139
|
+
constructor() {
|
|
140
|
+
super();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const db1 = new TestContext();
|
|
145
|
+
const db2 = new TestContext();
|
|
146
|
+
|
|
147
|
+
// Context 1: Generate hits/misses
|
|
148
|
+
const key = db1._queryCache.generateKey('query', [], 'users');
|
|
149
|
+
db1._queryCache.set(key, 'data', 'users');
|
|
150
|
+
db1._queryCache.get(key); // Hit
|
|
151
|
+
db1._queryCache.get('nonexistent'); // Miss
|
|
152
|
+
|
|
153
|
+
// Context 2: Should see same stats
|
|
154
|
+
const stats1 = db1.getCacheStats();
|
|
155
|
+
const stats2 = db2.getCacheStats();
|
|
156
|
+
|
|
157
|
+
if(stats1.hits === 1 && stats2.hits === 1 && stats1.misses === 1 && stats2.misses === 1) {
|
|
158
|
+
console.log(" ✓ Cache statistics shared across contexts");
|
|
159
|
+
console.log(` ✓ Both contexts see: ${stats1.hits} hit, ${stats1.misses} miss`);
|
|
160
|
+
passed++;
|
|
161
|
+
} else {
|
|
162
|
+
console.log(` ✗ Statistics not shared`);
|
|
163
|
+
console.log(` ✗ db1 stats: ${JSON.stringify(stats1)}`);
|
|
164
|
+
console.log(` ✗ db2 stats: ${JSON.stringify(stats2)}`);
|
|
165
|
+
failed++;
|
|
166
|
+
}
|
|
167
|
+
} catch(err) {
|
|
168
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
169
|
+
failed++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Test 5: Clear cache from one context affects all
|
|
173
|
+
console.log("\n📝 Test 5: Clear cache affects all contexts");
|
|
174
|
+
console.log("──────────────────────────────────────────────────");
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
class TestContext extends context {
|
|
178
|
+
constructor() {
|
|
179
|
+
super();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const db1 = new TestContext();
|
|
184
|
+
const db2 = new TestContext();
|
|
185
|
+
|
|
186
|
+
// Context 1: Add data
|
|
187
|
+
const key = db1._queryCache.generateKey('query', [], 'users');
|
|
188
|
+
db1._queryCache.set(key, 'data', 'users');
|
|
189
|
+
|
|
190
|
+
// Verify cached in both
|
|
191
|
+
const before1 = db1._queryCache.get(key);
|
|
192
|
+
const before2 = db2._queryCache.get(key);
|
|
193
|
+
|
|
194
|
+
// Context 2: Clear cache
|
|
195
|
+
db2.clearQueryCache();
|
|
196
|
+
|
|
197
|
+
// Both contexts should see empty cache
|
|
198
|
+
const after1 = db1._queryCache.get(key);
|
|
199
|
+
const after2 = db2._queryCache.get(key);
|
|
200
|
+
|
|
201
|
+
if(before1 !== null && before2 !== null && after1 === null && after2 === null) {
|
|
202
|
+
console.log(" ✓ Data cached in both contexts initially");
|
|
203
|
+
console.log(" ✓ Clear from context 2 affected context 1");
|
|
204
|
+
passed++;
|
|
205
|
+
} else {
|
|
206
|
+
console.log(` ✗ Clear not shared across contexts`);
|
|
207
|
+
failed++;
|
|
208
|
+
}
|
|
209
|
+
} catch(err) {
|
|
210
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
211
|
+
failed++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Summary
|
|
215
|
+
console.log("\n╔════════════════════════════════════════════════════════════════╗");
|
|
216
|
+
console.log("║ Test Summary ║");
|
|
217
|
+
console.log("╚════════════════════════════════════════════════════════════════╝");
|
|
218
|
+
console.log(`\n ✓ Passed: ${passed}`);
|
|
219
|
+
console.log(` ✗ Failed: ${failed}`);
|
|
220
|
+
console.log(` 📊 Total: ${passed + failed}\n`);
|
|
221
|
+
|
|
222
|
+
if(failed === 0) {
|
|
223
|
+
console.log(" 🎉 All tests passed!\n");
|
|
224
|
+
console.log(" ✅ Cache is properly shared across context instances");
|
|
225
|
+
console.log(" ✅ Bug fix verified: Multi-context cache invalidation works\n");
|
|
226
|
+
process.exit(0);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(" ❌ Some tests failed\n");
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Static Cache Sharing
|
|
3
|
+
* Verifies the cache is static/shared as designed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const QueryCache = require('../Cache/QueryCache');
|
|
7
|
+
|
|
8
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
9
|
+
console.log("║ Static Cache Test ║");
|
|
10
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
// Simulate context class with static shared cache
|
|
16
|
+
class SimulatedContext {
|
|
17
|
+
static _sharedQueryCache = null;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
// Initialize shared query cache (only once across all instances)
|
|
21
|
+
if (!SimulatedContext._sharedQueryCache) {
|
|
22
|
+
SimulatedContext._sharedQueryCache = new QueryCache({
|
|
23
|
+
ttl: 5 * 60 * 1000,
|
|
24
|
+
maxSize: 1000,
|
|
25
|
+
enabled: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Reference the shared cache
|
|
30
|
+
this._queryCache = SimulatedContext._sharedQueryCache;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getCacheStats() {
|
|
34
|
+
return this._queryCache.getStats();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clearQueryCache() {
|
|
38
|
+
this._queryCache.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Test 1: Multiple instances share the same cache
|
|
43
|
+
console.log("📝 Test 1: Multiple instances share same cache");
|
|
44
|
+
console.log("──────────────────────────────────────────────────");
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const ctx1 = new SimulatedContext();
|
|
48
|
+
const ctx2 = new SimulatedContext();
|
|
49
|
+
|
|
50
|
+
const areSame = ctx1._queryCache === ctx2._queryCache;
|
|
51
|
+
const isStatic = ctx1._queryCache === SimulatedContext._sharedQueryCache;
|
|
52
|
+
|
|
53
|
+
if(areSame && isStatic) {
|
|
54
|
+
console.log(" ✓ Both instances share the same cache");
|
|
55
|
+
console.log(" ✓ Cache is stored in static property");
|
|
56
|
+
passed++;
|
|
57
|
+
} else {
|
|
58
|
+
console.log(` ✗ Instances have separate caches`);
|
|
59
|
+
failed++;
|
|
60
|
+
}
|
|
61
|
+
} catch(err) {
|
|
62
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
63
|
+
failed++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Test 2: Cache SET in one instance visible in another
|
|
67
|
+
console.log("\n📝 Test 2: Cache operations are shared");
|
|
68
|
+
console.log("──────────────────────────────────────────────────");
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const ctx1 = new SimulatedContext();
|
|
72
|
+
const ctx2 = new SimulatedContext();
|
|
73
|
+
|
|
74
|
+
// Instance 1: Add to cache
|
|
75
|
+
const key = ctx1._queryCache.generateKey('SELECT * FROM users', [], 'users');
|
|
76
|
+
ctx1._queryCache.set(key, [{ id: 1, name: 'Alice' }], 'users');
|
|
77
|
+
|
|
78
|
+
// Instance 2: Should see the same cached data
|
|
79
|
+
const cached = ctx2._queryCache.get(key);
|
|
80
|
+
|
|
81
|
+
if(cached && cached[0].name === 'Alice') {
|
|
82
|
+
console.log(" ✓ Cache data visible across instances");
|
|
83
|
+
console.log(" ✓ Data written by ctx1, read by ctx2");
|
|
84
|
+
passed++;
|
|
85
|
+
} else {
|
|
86
|
+
console.log(` ✗ Cache data not shared`);
|
|
87
|
+
failed++;
|
|
88
|
+
}
|
|
89
|
+
} catch(err) {
|
|
90
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
91
|
+
failed++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Test 3: Cache invalidation in one instance affects another
|
|
95
|
+
console.log("\n📝 Test 3: Cache invalidation is shared");
|
|
96
|
+
console.log("──────────────────────────────────────────────────");
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const ctx1 = new SimulatedContext();
|
|
100
|
+
const ctx2 = new SimulatedContext();
|
|
101
|
+
|
|
102
|
+
// Clear for clean test
|
|
103
|
+
ctx1._queryCache.clear();
|
|
104
|
+
|
|
105
|
+
// Instance 1: Add entries
|
|
106
|
+
const key1 = ctx1._queryCache.generateKey('query1', [], 'users');
|
|
107
|
+
const key2 = ctx1._queryCache.generateKey('query2', [], 'users');
|
|
108
|
+
ctx1._queryCache.set(key1, { id: 1 }, 'users');
|
|
109
|
+
ctx1._queryCache.set(key2, { id: 2 }, 'users');
|
|
110
|
+
|
|
111
|
+
// Verify both cached
|
|
112
|
+
const before1 = ctx1._queryCache.get(key1);
|
|
113
|
+
const before2 = ctx2._queryCache.get(key2);
|
|
114
|
+
|
|
115
|
+
// Instance 2: Invalidate
|
|
116
|
+
ctx2._queryCache.invalidateTable('users');
|
|
117
|
+
|
|
118
|
+
// Both instances should see empty cache
|
|
119
|
+
const after1 = ctx1._queryCache.get(key1);
|
|
120
|
+
const after2 = ctx2._queryCache.get(key2);
|
|
121
|
+
|
|
122
|
+
if(before1 !== null && before2 !== null && after1 === null && after2 === null) {
|
|
123
|
+
console.log(" ✓ Invalidation in ctx2 affected ctx1");
|
|
124
|
+
console.log(" ✓ Cache properly shared");
|
|
125
|
+
passed++;
|
|
126
|
+
} else {
|
|
127
|
+
console.log(` ✗ Invalidation not shared`);
|
|
128
|
+
failed++;
|
|
129
|
+
}
|
|
130
|
+
} catch(err) {
|
|
131
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
132
|
+
failed++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Test 4: Statistics are shared
|
|
136
|
+
console.log("\n📝 Test 4: Statistics are shared");
|
|
137
|
+
console.log("──────────────────────────────────────────────────");
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const ctx1 = new SimulatedContext();
|
|
141
|
+
const ctx2 = new SimulatedContext();
|
|
142
|
+
|
|
143
|
+
// Clear for clean test
|
|
144
|
+
ctx1.clearQueryCache();
|
|
145
|
+
|
|
146
|
+
// Instance 1: Generate activity
|
|
147
|
+
const key = ctx1._queryCache.generateKey('test', [], 'users');
|
|
148
|
+
ctx1._queryCache.set(key, 'data', 'users');
|
|
149
|
+
ctx1._queryCache.get(key); // Hit
|
|
150
|
+
ctx1._queryCache.get('nonexistent'); // Miss
|
|
151
|
+
|
|
152
|
+
// Both instances should see same stats
|
|
153
|
+
const stats1 = ctx1.getCacheStats();
|
|
154
|
+
const stats2 = ctx2.getCacheStats();
|
|
155
|
+
|
|
156
|
+
if(stats1.hits === 1 && stats2.hits === 1 && stats1.misses === 1 && stats2.misses === 1) {
|
|
157
|
+
console.log(" ✓ Statistics shared across instances");
|
|
158
|
+
console.log(` ✓ Both see: ${stats1.hits} hit, ${stats1.misses} miss`);
|
|
159
|
+
passed++;
|
|
160
|
+
} else {
|
|
161
|
+
console.log(` ✗ Statistics not shared`);
|
|
162
|
+
failed++;
|
|
163
|
+
}
|
|
164
|
+
} catch(err) {
|
|
165
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
166
|
+
failed++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Summary
|
|
170
|
+
console.log("\n╔════════════════════════════════════════════════════════════════╗");
|
|
171
|
+
console.log("║ Test Summary ║");
|
|
172
|
+
console.log("╚════════════════════════════════════════════════════════════════╝");
|
|
173
|
+
console.log(`\n ✓ Passed: ${passed}`);
|
|
174
|
+
console.log(` ✗ Failed: ${failed}`);
|
|
175
|
+
console.log(` 📊 Total: ${passed + failed}\n`);
|
|
176
|
+
|
|
177
|
+
if(failed === 0) {
|
|
178
|
+
console.log(" 🎉 All tests passed!\n");
|
|
179
|
+
console.log(" ✅ Static cache pattern verified");
|
|
180
|
+
console.log(" ✅ BUG FIX CONFIRMED: Multi-context cache sharing works!\n");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(" ❌ Some tests failed\n");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const QueryCache = require('../Cache/QueryCache');
|
|
3
|
+
|
|
4
|
+
describe('QueryCache', () => {
|
|
5
|
+
let cache;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
cache = new QueryCache({ ttl: 1000, maxSize: 3 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('cache miss returns null', () => {
|
|
12
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
13
|
+
assert.strictEqual(cache.get(key), null);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('cache hit returns stored data', () => {
|
|
17
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
18
|
+
const data = [{ id: 1, name: 'John' }];
|
|
19
|
+
|
|
20
|
+
cache.set(key, data, 'users');
|
|
21
|
+
const cached = cache.get(key);
|
|
22
|
+
|
|
23
|
+
assert.deepStrictEqual(cached, data);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('TTL expiration removes entries', (done) => {
|
|
27
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
28
|
+
cache.set(key, { id: 1 }, 'users');
|
|
29
|
+
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
assert.strictEqual(cache.get(key), null);
|
|
32
|
+
done();
|
|
33
|
+
}, 1100); // Wait for TTL expiration
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('LRU eviction removes oldest entry', () => {
|
|
37
|
+
cache.set('key1', 'data1', 'table1');
|
|
38
|
+
cache.set('key2', 'data2', 'table2');
|
|
39
|
+
cache.set('key3', 'data3', 'table3');
|
|
40
|
+
|
|
41
|
+
// Access key1 to make it recently used
|
|
42
|
+
cache.get('key1');
|
|
43
|
+
|
|
44
|
+
// Add key4 - should evict key2 (least recently used)
|
|
45
|
+
cache.set('key4', 'data4', 'table4');
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(cache.get('key1'), 'data1'); // Still in cache
|
|
48
|
+
assert.strictEqual(cache.get('key2'), null); // Evicted
|
|
49
|
+
assert.strictEqual(cache.get('key3'), 'data3'); // Still in cache
|
|
50
|
+
assert.strictEqual(cache.get('key4'), 'data4'); // Newly added
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('invalidateTable removes all entries for table', () => {
|
|
54
|
+
cache.set('key1', 'data1', 'users');
|
|
55
|
+
cache.set('key2', 'data2', 'users');
|
|
56
|
+
cache.set('key3', 'data3', 'posts');
|
|
57
|
+
|
|
58
|
+
cache.invalidateTable('users');
|
|
59
|
+
|
|
60
|
+
assert.strictEqual(cache.get('key1'), null);
|
|
61
|
+
assert.strictEqual(cache.get('key2'), null);
|
|
62
|
+
assert.strictEqual(cache.get('key3'), 'data3'); // Different table
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('getStats returns accurate metrics', () => {
|
|
66
|
+
const key1 = cache.generateKey('query1', [], 'users');
|
|
67
|
+
const key2 = cache.generateKey('query2', [], 'posts');
|
|
68
|
+
|
|
69
|
+
cache.set(key1, 'data1', 'users');
|
|
70
|
+
cache.get(key1); // Hit
|
|
71
|
+
cache.get(key2); // Miss
|
|
72
|
+
|
|
73
|
+
const stats = cache.getStats();
|
|
74
|
+
assert.strictEqual(stats.size, 1);
|
|
75
|
+
assert.strictEqual(stats.hits, 1);
|
|
76
|
+
assert.strictEqual(stats.misses, 1);
|
|
77
|
+
assert.strictEqual(stats.hitRate, '50.00%');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('clear removes all cache entries', () => {
|
|
81
|
+
const key1 = cache.generateKey('query1', [], 'users');
|
|
82
|
+
const key2 = cache.generateKey('query2', [], 'posts');
|
|
83
|
+
|
|
84
|
+
cache.set(key1, 'data1', 'users');
|
|
85
|
+
cache.set(key2, 'data2', 'posts');
|
|
86
|
+
|
|
87
|
+
assert.strictEqual(cache.cache.size, 2);
|
|
88
|
+
|
|
89
|
+
cache.clear();
|
|
90
|
+
|
|
91
|
+
assert.strictEqual(cache.cache.size, 0);
|
|
92
|
+
assert.strictEqual(cache.hitCount, 0);
|
|
93
|
+
assert.strictEqual(cache.missCount, 0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('disabled cache does not store or retrieve data', () => {
|
|
97
|
+
cache.enabled = false;
|
|
98
|
+
|
|
99
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
100
|
+
cache.set(key, 'data', 'users');
|
|
101
|
+
|
|
102
|
+
assert.strictEqual(cache.get(key), null);
|
|
103
|
+
assert.strictEqual(cache.cache.size, 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('generateKey creates consistent keys for same query', () => {
|
|
107
|
+
const key1 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
|
|
108
|
+
const key2 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
|
|
109
|
+
const key3 = cache.generateKey('SELECT * FROM users WHERE id = ?', [2], 'users');
|
|
110
|
+
|
|
111
|
+
assert.strictEqual(key1, key2); // Same query + params = same key
|
|
112
|
+
assert.notStrictEqual(key1, key3); // Different params = different key
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('generateKey normalizes whitespace', () => {
|
|
116
|
+
const key1 = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
117
|
+
const key2 = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
118
|
+
|
|
119
|
+
assert.strictEqual(key1, key2); // Whitespace normalized
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('cache updates lastAccess on hit', () => {
|
|
123
|
+
const key = cache.generateKey('query', [], 'users');
|
|
124
|
+
cache.set(key, 'data', 'users');
|
|
125
|
+
|
|
126
|
+
const entry1 = cache.cache.get(key);
|
|
127
|
+
const originalAccess = entry1.lastAccess;
|
|
128
|
+
|
|
129
|
+
// Wait a bit
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
cache.get(key);
|
|
132
|
+
const entry2 = cache.cache.get(key);
|
|
133
|
+
assert(entry2.lastAccess > originalAccess);
|
|
134
|
+
}, 10);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('cache tracks hit count per entry', () => {
|
|
138
|
+
const key = cache.generateKey('query', [], 'users');
|
|
139
|
+
cache.set(key, 'data', 'users');
|
|
140
|
+
|
|
141
|
+
cache.get(key);
|
|
142
|
+
cache.get(key);
|
|
143
|
+
cache.get(key);
|
|
144
|
+
|
|
145
|
+
const entry = cache.cache.get(key);
|
|
146
|
+
assert.strictEqual(entry.hits, 3);
|
|
147
|
+
});
|
|
148
|
+
});
|