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/readme.md CHANGED
@@ -10,6 +10,7 @@
10
10
  šŸ”¹ **Multi-Database Support** - MySQL, PostgreSQL, SQLite with consistent API
11
11
  šŸ”¹ **Code-First Design** - Define entities in JavaScript, generate schema automatically
12
12
  šŸ”¹ **Fluent Query API** - Lambda-based queries with parameterized placeholders
13
+ šŸ”¹ **Query Result Caching** - Production-grade in-memory and Redis caching with automatic invalidation
13
14
  šŸ”¹ **Migration System** - CLI-driven migrations with rollback support
14
15
  šŸ”¹ **SQL Injection Protection** - Automatic parameterized queries throughout
15
16
  šŸ”¹ **Field Transformers** - Custom serialization/deserialization for complex types
@@ -34,8 +35,16 @@
34
35
  - [Querying](#querying)
35
36
  - [Migrations](#migrations)
36
37
  - [Advanced Features](#advanced-features)
38
+ - [Query Result Caching](#query-result-caching)
39
+ - [Field Transformers](#field-transformers-advanced)
40
+ - [Table Prefixes](#table-prefixes)
41
+ - [Transactions](#transactions-postgresql)
42
+ - [Multi-Context Applications](#multi-context-applications)
43
+ - [Raw SQL Queries](#raw-sql-queries)
37
44
  - [API Reference](#api-reference)
38
45
  - [Examples](#examples)
46
+ - [Performance Tips](#performance-tips)
47
+ - [Security](#security)
39
48
 
40
49
  ## Installation
41
50
 
@@ -754,6 +763,264 @@ const result = await connection.transaction(async (client) => {
754
763
  // Automatically commits on success, rolls back on error
755
764
  ```
756
765
 
766
+ ### Query Result Caching
767
+
768
+ MasterRecord includes a **production-grade two-level caching system** similar to Entity Framework and Hibernate. The cache dramatically improves performance by storing query results and automatically invalidating them when data changes.
769
+
770
+ #### How It Works
771
+
772
+ ```
773
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
774
+ │ First-Level Cache (Identity Map) │
775
+ │ - Request-scoped entity tracking │
776
+ │ - O(1) entity lookup │
777
+ │ - Already in MasterRecord │
778
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
779
+ ā–¼
780
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
781
+ │ Second-Level Cache (Query Result Cache) │
782
+ │ - Application-wide query result storage │
783
+ │ - Automatic invalidation on data changes │
784
+ │ - In-memory (development) or Redis (production) │
785
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
786
+ ```
787
+
788
+ #### Basic Usage (Default Behavior)
789
+
790
+ Caching is **enabled by default** and requires zero configuration. The cache is **shared across all context instances** to ensure consistency:
791
+
792
+ ```javascript
793
+ const db = new AppContext();
794
+
795
+ // First query hits database (cache miss)
796
+ const user = db.User.where(u => u.id == $$, 1).single();
797
+
798
+ // Second identical query hits cache (99%+ faster)
799
+ const user2 = db.User.where(u => u.id == $$, 1).single();
800
+
801
+ // Update invalidates cache automatically
802
+ user2.name = "Updated";
803
+ db.saveChanges(); // Cache for User table cleared
804
+
805
+ // Next query hits database again (cache miss)
806
+ const user3 = db.User.where(u => u.id == $$, 1).single();
807
+
808
+ // Cache is shared across all context instances
809
+ const db2 = new AppContext();
810
+ const user4 = db2.User.findById(1); // Also uses shared cache
811
+ ```
812
+
813
+ #### Configuration
814
+
815
+ Configure caching via environment variables:
816
+
817
+ ```bash
818
+ # Development (.env)
819
+ QUERY_CACHE_ENABLED=true # Enable/disable (default: true)
820
+ QUERY_CACHE_TTL=300000 # TTL in milliseconds (default: 5 minutes)
821
+ QUERY_CACHE_SIZE=1000 # Max cache entries (default: 1000)
822
+
823
+ # Production (.env)
824
+ QUERY_CACHE_ENABLED=true
825
+ QUERY_CACHE_TTL=300 # Redis uses seconds
826
+ REDIS_URL=redis://localhost:6379 # Use Redis for distributed caching
827
+ ```
828
+
829
+ #### Disable Caching for Specific Queries
830
+
831
+ Use `.noCache()` for real-time data that shouldn't be cached:
832
+
833
+ ```javascript
834
+ // Always hit database (never cached)
835
+ const liveData = db.Analytics
836
+ .where(a => a.date == $$, today)
837
+ .noCache() // Skip cache
838
+ .toList();
839
+
840
+ // Reference data (highly cacheable)
841
+ const categories = db.Categories.toList(); // Cached for 5 minutes
842
+ ```
843
+
844
+ #### Manual Cache Control
845
+
846
+ ```javascript
847
+ const db = new AppContext();
848
+
849
+ // Check cache performance
850
+ const stats = db.getCacheStats();
851
+ console.log(stats);
852
+ // {
853
+ // size: 45,
854
+ // maxSize: 1000,
855
+ // hits: 234,
856
+ // misses: 67,
857
+ // hitRate: '77.74%',
858
+ // enabled: true
859
+ // }
860
+
861
+ // Clear cache manually
862
+ db.clearQueryCache();
863
+
864
+ // Disable caching temporarily
865
+ db.setQueryCacheEnabled(false);
866
+ const freshData = db.User.toList();
867
+ db.setQueryCacheEnabled(true);
868
+ ```
869
+
870
+ #### Redis-Based Distributed Caching (Production)
871
+
872
+ For multi-process or clustered deployments, use Redis:
873
+
874
+ ```javascript
875
+ const redis = require('redis');
876
+ const RedisQueryCache = require('masterrecord/Cache/RedisQueryCache');
877
+
878
+ class AppContext extends context {
879
+ constructor() {
880
+ super();
881
+
882
+ // Use Redis cache in production
883
+ if (process.env.NODE_ENV === 'production' && process.env.REDIS_URL) {
884
+ const redisClient = redis.createClient(process.env.REDIS_URL);
885
+ this._queryCache = new RedisQueryCache(redisClient, {
886
+ ttl: 300, // 5 minutes (seconds for Redis)
887
+ prefix: 'myapp:'
888
+ });
889
+ }
890
+ // In-memory cache used automatically in development
891
+
892
+ this.dbset(User);
893
+ }
894
+ }
895
+ ```
896
+
897
+ **Benefits of Redis cache:**
898
+ - Shared across processes (horizontally scalable)
899
+ - Pub/sub invalidation (cache stays consistent)
900
+ - Two-level cache (L1 in-memory + L2 Redis)
901
+ - Automatic failover to database on Redis errors
902
+
903
+ #### Cache Invalidation Strategy
904
+
905
+ MasterRecord automatically invalidates cache entries when data changes:
906
+
907
+ ```javascript
908
+ // Query is cached
909
+ const users = db.User.where(u => u.active == true).toList();
910
+
911
+ // Any modification to User table invalidates ALL User queries
912
+ const user = db.User.findById(1);
913
+ user.name = "Updated";
914
+ db.saveChanges(); // Invalidates all cached User queries
915
+
916
+ // Next query hits database (fresh data)
917
+ const usersAgain = db.User.where(u => u.active == true).toList();
918
+
919
+ // Queries for OTHER tables are unaffected
920
+ const posts = db.Post.toList(); // Still cached
921
+ ```
922
+
923
+ **Invalidation rules:**
924
+ - `INSERT` invalidates all queries for that table
925
+ - `UPDATE` invalidates all queries for that table
926
+ - `DELETE` invalidates all queries for that table
927
+ - Queries for other tables are not affected
928
+
929
+ #### Performance Impact
930
+
931
+ Expected performance improvements:
932
+
933
+ | Scenario | Without Cache | With Cache | Improvement |
934
+ |----------|---------------|------------|-------------|
935
+ | Single query (100 calls) | 100 DB queries | 1 DB + 99 cache | **99% faster** |
936
+ | List query (50 calls) | 50 DB queries | 1 DB + 49 cache | **98% faster** |
937
+ | Reference data (1000 calls) | 1000 DB queries | 1 DB + 999 cache | **99.9% faster** |
938
+ | Mixed operations | Baseline | 70-90% hit rate | **3-10x faster** |
939
+
940
+ **Memory usage:** ~1KB per cached query (1000 entries ā‰ˆ 1MB)
941
+
942
+ #### Best Practices
943
+
944
+ **DO cache:**
945
+ ```javascript
946
+ // Reference data (rarely changes)
947
+ const categories = db.Categories.toList();
948
+ const settings = db.Settings.toList();
949
+
950
+ // Read-heavy data (user profiles)
951
+ const user = db.User.findById(userId);
952
+
953
+ // Expensive aggregations
954
+ const stats = db.Orders
955
+ .where(o => o.status == $$, 'completed')
956
+ .count();
957
+ ```
958
+
959
+ **DON'T cache:**
960
+ ```javascript
961
+ // Real-time data (always needs fresh results)
962
+ const liveOrders = db.Orders
963
+ .where(o => o.status == $$, 'pending')
964
+ .noCache()
965
+ .toList();
966
+
967
+ // Financial transactions (critical accuracy)
968
+ const balance = db.Transactions
969
+ .where(t => t.user_id == $$, userId)
970
+ .noCache()
971
+ .toList();
972
+
973
+ // User-specific sensitive data (security concern)
974
+ const permissions = db.UserPermissions
975
+ .where(p => p.user_id == $$, userId)
976
+ .noCache()
977
+ .toList();
978
+ ```
979
+
980
+ #### Monitoring Cache Performance
981
+
982
+ ```javascript
983
+ // Log cache stats periodically
984
+ setInterval(() => {
985
+ const stats = db.getCacheStats();
986
+ console.log(`Cache: ${stats.hitRate} hit rate, ${stats.size}/${stats.maxSize} entries`);
987
+ }, 60000);
988
+
989
+ // Watch for low hit rates (< 50% might indicate poor cache strategy)
990
+ if (parseFloat(stats.hitRate) < 50) {
991
+ console.warn('Cache hit rate is low, consider tuning cache TTL or size');
992
+ }
993
+ ```
994
+
995
+ #### Important: Shared Cache Behavior
996
+
997
+ **The cache is shared across all context instances of the same class.** This ensures consistency:
998
+
999
+ ```javascript
1000
+ const db1 = new AppContext();
1001
+ const db2 = new AppContext();
1002
+
1003
+ // Context 1: Cache data
1004
+ const user1 = db1.User.findById(1); // DB query, cached
1005
+
1006
+ // Context 2: Sees cached data
1007
+ const user2 = db2.User.findById(1); // Cache hit!
1008
+
1009
+ // Context 2: Updates invalidate cache for BOTH contexts
1010
+ user2.name = "Updated";
1011
+ db2.saveChanges(); // Invalidates shared cache
1012
+
1013
+ // Context 1: Sees fresh data
1014
+ const user3 = db1.User.findById(1); // Cache miss, fresh data
1015
+ console.log(user3.name); // "Updated"
1016
+ ```
1017
+
1018
+ **Why shared cache?**
1019
+ - āœ… Prevents stale data across multiple context instances
1020
+ - āœ… Ensures all parts of your application see consistent data
1021
+ - āœ… Reduces memory usage (one cache instead of many)
1022
+ - āœ… Correct behavior for single-database applications (most use cases)
1023
+
757
1024
  ### Multi-Context Applications
758
1025
 
759
1026
  Manage multiple databases in one application:
@@ -827,6 +1094,11 @@ context.saveChanges() // MySQL/SQLite (sync)
827
1094
  // Add/Remove entities
828
1095
  context.EntityName.add(entity)
829
1096
  context.remove(entity)
1097
+
1098
+ // Cache management
1099
+ context.getCacheStats() // Get cache statistics
1100
+ context.clearQueryCache() // Clear all cached queries
1101
+ context.setQueryCacheEnabled(bool) // Enable/disable caching
830
1102
  ```
831
1103
 
832
1104
  ### Query Methods
@@ -840,6 +1112,7 @@ context.remove(entity)
840
1112
  .skip(number) // Skip N records
841
1113
  .take(number) // Limit to N records
842
1114
  .include(relationship) // Eager load
1115
+ .noCache() // Disable caching for this query
843
1116
 
844
1117
  // Terminal methods (execute query)
845
1118
  .toList() // Return array
@@ -1009,7 +1282,25 @@ console.log(`${author.name} has ${posts.length} posts`);
1009
1282
 
1010
1283
  ## Performance Tips
1011
1284
 
1012
- ### 1. Use Bulk Operations
1285
+ ### 1. Leverage Query Caching
1286
+
1287
+ ```javascript
1288
+ // āœ… GOOD: Cache reference data
1289
+ const categories = db.Categories.toList(); // Cached automatically
1290
+
1291
+ // āœ… GOOD: Reuse queries (cache hits)
1292
+ const user1 = db.User.findById(123); // DB query
1293
+ const user2 = db.User.findById(123); // Cache hit (instant)
1294
+
1295
+ // āœ… GOOD: Disable cache for real-time data
1296
+ const liveOrders = db.Orders.where(o => o.status == 'pending').noCache().toList();
1297
+
1298
+ // Monitor cache performance
1299
+ const stats = db.getCacheStats();
1300
+ console.log(`Cache hit rate: ${stats.hitRate}`); // Target: > 70%
1301
+ ```
1302
+
1303
+ ### 2. Use Bulk Operations
1013
1304
 
1014
1305
  ```javascript
1015
1306
  // āŒ BAD: Multiple inserts
@@ -1027,7 +1318,7 @@ for (const item of items) {
1027
1318
  await db.saveChanges(); // Batch insert
1028
1319
  ```
1029
1320
 
1030
- ### 2. Use Indexes
1321
+ ### 3. Use Indexes
1031
1322
 
1032
1323
  ```javascript
1033
1324
  class User {
@@ -1043,7 +1334,7 @@ class User {
1043
1334
  // CREATE INDEX idx_user_status ON User(status);
1044
1335
  ```
1045
1336
 
1046
- ### 3. Limit Result Sets
1337
+ ### 4. Limit Result Sets
1047
1338
 
1048
1339
  ```javascript
1049
1340
  // āœ… GOOD: Limit results
@@ -1056,7 +1347,7 @@ const recentUsers = db.User
1056
1347
  const allUsers = db.User.all();
1057
1348
  ```
1058
1349
 
1059
- ### 4. Use Connection Pooling (PostgreSQL)
1350
+ ### 5. Use Connection Pooling (PostgreSQL)
1060
1351
 
1061
1352
  ```javascript
1062
1353
  this.env({
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Test: Query Cache Integration
3
+ *
4
+ * Tests the query result caching system integration points
5
+ */
6
+
7
+ const QueryCache = require('../Cache/QueryCache');
8
+
9
+ console.log("╔════════════════════════════════════════════════════════════════╗");
10
+ console.log("ā•‘ Query Cache Integration Test ā•‘");
11
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
12
+
13
+ let passed = 0;
14
+ let failed = 0;
15
+
16
+ // Test 1: Query cache can be instantiated
17
+ console.log("šŸ“ Test 1: Query cache instantiation");
18
+ console.log("──────────────────────────────────────────────────");
19
+
20
+ try {
21
+ const cache = new QueryCache({ ttl: 5000, maxSize: 100 });
22
+
23
+ if(cache.enabled && cache.ttl === 5000 && cache.maxSize === 100) {
24
+ console.log(" āœ“ Query cache instantiated successfully");
25
+ console.log(" āœ“ Configuration options applied correctly");
26
+ passed++;
27
+ } else {
28
+ console.log(` āœ— Cache configuration incorrect`);
29
+ failed++;
30
+ }
31
+ } catch(err) {
32
+ console.log(` āœ— Error: ${err.message}`);
33
+ failed++;
34
+ }
35
+
36
+ // Test 2: Cache key generation is deterministic
37
+ console.log("\nšŸ“ Test 2: Cache key generation");
38
+ console.log("──────────────────────────────────────────────────");
39
+
40
+ try {
41
+ const cache = new QueryCache();
42
+
43
+ const key1 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
44
+ const key2 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
45
+ const key3 = cache.generateKey('SELECT * FROM users WHERE id = ?', [2], 'users');
46
+
47
+ if(key1 === key2 && key1 !== key3) {
48
+ console.log(" āœ“ Cache keys are deterministic (same input = same key)");
49
+ console.log(" āœ“ Different parameters produce different keys");
50
+ passed++;
51
+ } else {
52
+ console.log(` āœ— Cache key generation not working correctly`);
53
+ failed++;
54
+ }
55
+ } catch(err) {
56
+ console.log(` āœ— Error: ${err.message}`);
57
+ failed++;
58
+ }
59
+
60
+ // Test 3: Cache set and get operations
61
+ console.log("\nšŸ“ Test 3: Cache set and get operations");
62
+ console.log("──────────────────────────────────────────────────");
63
+
64
+ try {
65
+ const cache = new QueryCache();
66
+ const key = cache.generateKey('SELECT * FROM users', [], 'users');
67
+ const data = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
68
+
69
+ cache.set(key, data, 'users');
70
+ const retrieved = cache.get(key);
71
+
72
+ if(JSON.stringify(retrieved) === JSON.stringify(data)) {
73
+ console.log(" āœ“ Data stored in cache successfully");
74
+ console.log(" āœ“ Data retrieved from cache matches stored data");
75
+ passed++;
76
+ } else {
77
+ console.log(` āœ— Retrieved data doesn't match stored data`);
78
+ failed++;
79
+ }
80
+ } catch(err) {
81
+ console.log(` āœ— Error: ${err.message}`);
82
+ failed++;
83
+ }
84
+
85
+ // Test 4: Cache miss returns null
86
+ console.log("\nšŸ“ Test 4: Cache miss behavior");
87
+ console.log("──────────────────────────────────────────────────");
88
+
89
+ try {
90
+ const cache = new QueryCache();
91
+ const key = cache.generateKey('SELECT * FROM users WHERE id = 999', [], 'users');
92
+
93
+ const result = cache.get(key);
94
+
95
+ if(result === null) {
96
+ console.log(" āœ“ Cache miss returns null");
97
+ console.log(" āœ“ Miss count incremented");
98
+ passed++;
99
+ } else {
100
+ console.log(` āœ— Cache miss didn't return null`);
101
+ failed++;
102
+ }
103
+ } catch(err) {
104
+ console.log(` āœ— Error: ${err.message}`);
105
+ failed++;
106
+ }
107
+
108
+ // Test 5: Cache invalidation by table
109
+ console.log("\nšŸ“ Test 5: Table-based cache invalidation");
110
+ console.log("──────────────────────────────────────────────────");
111
+
112
+ try {
113
+ const cache = new QueryCache();
114
+
115
+ const key1 = cache.generateKey('SELECT * FROM users', [], 'users');
116
+ const key2 = cache.generateKey('SELECT * FROM users WHERE id = 1', [], 'users');
117
+ const key3 = cache.generateKey('SELECT * FROM posts', [], 'posts');
118
+
119
+ cache.set(key1, [{ id: 1 }], 'users');
120
+ cache.set(key2, { id: 1 }, 'users');
121
+ cache.set(key3, [{ id: 1 }], 'posts');
122
+
123
+ cache.invalidateTable('users');
124
+
125
+ const result1 = cache.get(key1);
126
+ const result2 = cache.get(key2);
127
+ const result3 = cache.get(key3);
128
+
129
+ if(result1 === null && result2 === null && result3 !== null) {
130
+ console.log(" āœ“ Invalidated all entries for 'users' table");
131
+ console.log(" āœ“ Did not invalidate 'posts' table entries");
132
+ passed++;
133
+ } else {
134
+ console.log(` āœ— Invalidation didn't work as expected`);
135
+ failed++;
136
+ }
137
+ } catch(err) {
138
+ console.log(` āœ— Error: ${err.message}`);
139
+ failed++;
140
+ }
141
+
142
+ // Test 6: Cache statistics tracking
143
+ console.log("\nšŸ“ Test 6: Cache statistics");
144
+ console.log("──────────────────────────────────────────────────");
145
+
146
+ try {
147
+ const cache = new QueryCache();
148
+ const key = cache.generateKey('query', [], 'users');
149
+
150
+ cache.set(key, 'data', 'users');
151
+ cache.get(key); // Hit
152
+ cache.get('nonexistent'); // Miss
153
+
154
+ const stats = cache.getStats();
155
+
156
+ if(stats.hits === 1 && stats.misses === 1 && stats.hitRate === '50.00%') {
157
+ console.log(" āœ“ Hit count tracked correctly");
158
+ console.log(" āœ“ Miss count tracked correctly");
159
+ console.log(` āœ“ Hit rate calculated correctly: ${stats.hitRate}`);
160
+ passed++;
161
+ } else {
162
+ console.log(` āœ— Stats incorrect: ${JSON.stringify(stats)}`);
163
+ failed++;
164
+ }
165
+ } catch(err) {
166
+ console.log(` āœ— Error: ${err.message}`);
167
+ failed++;
168
+ }
169
+
170
+ // Test 7: Cache can be cleared
171
+ console.log("\nšŸ“ Test 7: Cache clearing");
172
+ console.log("──────────────────────────────────────────────────");
173
+
174
+ try {
175
+ const cache = new QueryCache();
176
+ const key = cache.generateKey('query', [], 'users');
177
+
178
+ cache.set(key, 'data', 'users');
179
+ cache.get(key);
180
+
181
+ const statsBefore = cache.getStats();
182
+ cache.clear();
183
+ const statsAfter = cache.getStats();
184
+
185
+ if(statsAfter.size === 0 && statsAfter.hits === 0 && statsAfter.misses === 0) {
186
+ console.log(" āœ“ All cache entries removed");
187
+ console.log(" āœ“ Statistics reset to zero");
188
+ passed++;
189
+ } else {
190
+ console.log(` āœ— Cache not properly cleared`);
191
+ failed++;
192
+ }
193
+ } catch(err) {
194
+ console.log(` āœ— Error: ${err.message}`);
195
+ failed++;
196
+ }
197
+
198
+ // Test 8: Cache can be disabled
199
+ console.log("\nšŸ“ Test 8: Cache enable/disable");
200
+ console.log("──────────────────────────────────────────────────");
201
+
202
+ try {
203
+ const cache = new QueryCache({ enabled: false });
204
+ const key = cache.generateKey('query', [], 'users');
205
+
206
+ cache.set(key, 'data', 'users');
207
+ const result = cache.get(key);
208
+
209
+ if(result === null && cache.cache.size === 0) {
210
+ console.log(" āœ“ Disabled cache doesn't store data");
211
+ console.log(" āœ“ Disabled cache always returns null");
212
+ passed++;
213
+ } else {
214
+ console.log(` āœ— Disabled cache still storing data`);
215
+ failed++;
216
+ }
217
+ } catch(err) {
218
+ console.log(` āœ— Error: ${err.message}`);
219
+ failed++;
220
+ }
221
+
222
+ // Test 9: LRU eviction works
223
+ console.log("\nšŸ“ Test 9: LRU eviction");
224
+ console.log("──────────────────────────────────────────────────");
225
+
226
+ try {
227
+ const cache = new QueryCache({ maxSize: 3 });
228
+
229
+ cache.set('key1', 'data1', 'table1');
230
+
231
+ // Small delay to ensure different timestamps
232
+ const delay = (ms) => {
233
+ const start = Date.now();
234
+ while (Date.now() - start < ms) {}
235
+ };
236
+
237
+ delay(2);
238
+ cache.set('key2', 'data2', 'table2');
239
+ delay(2);
240
+ cache.set('key3', 'data3', 'table3');
241
+
242
+ // Access key1 to make it recently used
243
+ cache.get('key1');
244
+
245
+ // Add key4 - should evict key2 (least recently used)
246
+ cache.set('key4', 'data4', 'table4');
247
+
248
+ const key1Result = cache.get('key1');
249
+ const key2Result = cache.get('key2');
250
+ const key3Result = cache.get('key3');
251
+ const key4Result = cache.get('key4');
252
+
253
+ if(key1Result !== null && key2Result === null && key3Result !== null && key4Result !== null) {
254
+ console.log(" āœ“ LRU eviction removed least recently used entry");
255
+ console.log(" āœ“ Recently accessed entries preserved");
256
+ passed++;
257
+ } else {
258
+ console.log(` āœ— LRU eviction didn't work correctly`);
259
+ console.log(` āœ— Debug: key1=${key1Result !== null}, key2=${key2Result !== null}, key3=${key3Result !== null}, key4=${key4Result !== null}`);
260
+ failed++;
261
+ }
262
+ } catch(err) {
263
+ console.log(` āœ— Error: ${err.message}`);
264
+ failed++;
265
+ }
266
+
267
+ // Test 10: TTL expiration (short test)
268
+ console.log("\nšŸ“ Test 10: TTL expiration");
269
+ console.log("──────────────────────────────────────────────────");
270
+
271
+ try {
272
+ const cache = new QueryCache({ ttl: 100 }); // 100ms TTL
273
+ const key = cache.generateKey('query', [], 'users');
274
+
275
+ cache.set(key, 'data', 'users');
276
+
277
+ // Should be cached immediately
278
+ const result1 = cache.get(key);
279
+
280
+ // Wait for TTL expiration
281
+ setTimeout(() => {
282
+ const result2 = cache.get(key);
283
+
284
+ if(result1 !== null && result2 === null) {
285
+ console.log(" āœ“ Data cached initially");
286
+ console.log(" āœ“ Data expired after TTL");
287
+ passed++;
288
+ } else {
289
+ console.log(` āœ— TTL expiration didn't work correctly`);
290
+ failed++;
291
+ }
292
+
293
+ // Continue with summary after async test
294
+ printSummary();
295
+ }, 150);
296
+ } catch(err) {
297
+ console.log(` āœ— Error: ${err.message}`);
298
+ failed++;
299
+ printSummary();
300
+ }
301
+
302
+ function printSummary() {
303
+
304
+ // Summary
305
+ console.log("\n╔════════════════════════════════════════════════════════════════╗");
306
+ console.log("ā•‘ Test Summary ā•‘");
307
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
308
+ console.log(`\n āœ“ Passed: ${passed}`);
309
+ console.log(` āœ— Failed: ${failed}`);
310
+ console.log(` šŸ“Š Total: ${passed + failed}\n`);
311
+
312
+ if(failed === 0) {
313
+ console.log(" šŸŽ‰ All tests passed!\n");
314
+ process.exit(0);
315
+ } else {
316
+ console.log(" āŒ Some tests failed\n");
317
+ process.exit(1);
318
+ }
319
+ }