masterrecord 0.3.5 ā 0.3.6
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 +43 -0
- package/package.json +1 -1
- package/readme.md +262 -4
- package/test/cacheIntegration.test.js +319 -0
- package/test/queryCache.test.js +148 -0
- package/examples/jsonArrayTransformer.js +0 -215
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,231 @@ 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:
|
|
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
|
+
|
|
809
|
+
#### Configuration
|
|
810
|
+
|
|
811
|
+
Configure caching via environment variables:
|
|
812
|
+
|
|
813
|
+
```bash
|
|
814
|
+
# Development (.env)
|
|
815
|
+
QUERY_CACHE_ENABLED=true # Enable/disable (default: true)
|
|
816
|
+
QUERY_CACHE_TTL=300000 # TTL in milliseconds (default: 5 minutes)
|
|
817
|
+
QUERY_CACHE_SIZE=1000 # Max cache entries (default: 1000)
|
|
818
|
+
|
|
819
|
+
# Production (.env)
|
|
820
|
+
QUERY_CACHE_ENABLED=true
|
|
821
|
+
QUERY_CACHE_TTL=300 # Redis uses seconds
|
|
822
|
+
REDIS_URL=redis://localhost:6379 # Use Redis for distributed caching
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
#### Disable Caching for Specific Queries
|
|
826
|
+
|
|
827
|
+
Use `.noCache()` for real-time data that shouldn't be cached:
|
|
828
|
+
|
|
829
|
+
```javascript
|
|
830
|
+
// Always hit database (never cached)
|
|
831
|
+
const liveData = db.Analytics
|
|
832
|
+
.where(a => a.date == $$, today)
|
|
833
|
+
.noCache() // Skip cache
|
|
834
|
+
.toList();
|
|
835
|
+
|
|
836
|
+
// Reference data (highly cacheable)
|
|
837
|
+
const categories = db.Categories.toList(); // Cached for 5 minutes
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
#### Manual Cache Control
|
|
841
|
+
|
|
842
|
+
```javascript
|
|
843
|
+
const db = new AppContext();
|
|
844
|
+
|
|
845
|
+
// Check cache performance
|
|
846
|
+
const stats = db.getCacheStats();
|
|
847
|
+
console.log(stats);
|
|
848
|
+
// {
|
|
849
|
+
// size: 45,
|
|
850
|
+
// maxSize: 1000,
|
|
851
|
+
// hits: 234,
|
|
852
|
+
// misses: 67,
|
|
853
|
+
// hitRate: '77.74%',
|
|
854
|
+
// enabled: true
|
|
855
|
+
// }
|
|
856
|
+
|
|
857
|
+
// Clear cache manually
|
|
858
|
+
db.clearQueryCache();
|
|
859
|
+
|
|
860
|
+
// Disable caching temporarily
|
|
861
|
+
db.setQueryCacheEnabled(false);
|
|
862
|
+
const freshData = db.User.toList();
|
|
863
|
+
db.setQueryCacheEnabled(true);
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
#### Redis-Based Distributed Caching (Production)
|
|
867
|
+
|
|
868
|
+
For multi-process or clustered deployments, use Redis:
|
|
869
|
+
|
|
870
|
+
```javascript
|
|
871
|
+
const redis = require('redis');
|
|
872
|
+
const RedisQueryCache = require('masterrecord/Cache/RedisQueryCache');
|
|
873
|
+
|
|
874
|
+
class AppContext extends context {
|
|
875
|
+
constructor() {
|
|
876
|
+
super();
|
|
877
|
+
|
|
878
|
+
// Use Redis cache in production
|
|
879
|
+
if (process.env.NODE_ENV === 'production' && process.env.REDIS_URL) {
|
|
880
|
+
const redisClient = redis.createClient(process.env.REDIS_URL);
|
|
881
|
+
this._queryCache = new RedisQueryCache(redisClient, {
|
|
882
|
+
ttl: 300, // 5 minutes (seconds for Redis)
|
|
883
|
+
prefix: 'myapp:'
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
// In-memory cache used automatically in development
|
|
887
|
+
|
|
888
|
+
this.dbset(User);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
**Benefits of Redis cache:**
|
|
894
|
+
- Shared across processes (horizontally scalable)
|
|
895
|
+
- Pub/sub invalidation (cache stays consistent)
|
|
896
|
+
- Two-level cache (L1 in-memory + L2 Redis)
|
|
897
|
+
- Automatic failover to database on Redis errors
|
|
898
|
+
|
|
899
|
+
#### Cache Invalidation Strategy
|
|
900
|
+
|
|
901
|
+
MasterRecord automatically invalidates cache entries when data changes:
|
|
902
|
+
|
|
903
|
+
```javascript
|
|
904
|
+
// Query is cached
|
|
905
|
+
const users = db.User.where(u => u.active == true).toList();
|
|
906
|
+
|
|
907
|
+
// Any modification to User table invalidates ALL User queries
|
|
908
|
+
const user = db.User.findById(1);
|
|
909
|
+
user.name = "Updated";
|
|
910
|
+
db.saveChanges(); // Invalidates all cached User queries
|
|
911
|
+
|
|
912
|
+
// Next query hits database (fresh data)
|
|
913
|
+
const usersAgain = db.User.where(u => u.active == true).toList();
|
|
914
|
+
|
|
915
|
+
// Queries for OTHER tables are unaffected
|
|
916
|
+
const posts = db.Post.toList(); // Still cached
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**Invalidation rules:**
|
|
920
|
+
- `INSERT` invalidates all queries for that table
|
|
921
|
+
- `UPDATE` invalidates all queries for that table
|
|
922
|
+
- `DELETE` invalidates all queries for that table
|
|
923
|
+
- Queries for other tables are not affected
|
|
924
|
+
|
|
925
|
+
#### Performance Impact
|
|
926
|
+
|
|
927
|
+
Expected performance improvements:
|
|
928
|
+
|
|
929
|
+
| Scenario | Without Cache | With Cache | Improvement |
|
|
930
|
+
|----------|---------------|------------|-------------|
|
|
931
|
+
| Single query (100 calls) | 100 DB queries | 1 DB + 99 cache | **99% faster** |
|
|
932
|
+
| List query (50 calls) | 50 DB queries | 1 DB + 49 cache | **98% faster** |
|
|
933
|
+
| Reference data (1000 calls) | 1000 DB queries | 1 DB + 999 cache | **99.9% faster** |
|
|
934
|
+
| Mixed operations | Baseline | 70-90% hit rate | **3-10x faster** |
|
|
935
|
+
|
|
936
|
+
**Memory usage:** ~1KB per cached query (1000 entries ā 1MB)
|
|
937
|
+
|
|
938
|
+
#### Best Practices
|
|
939
|
+
|
|
940
|
+
**DO cache:**
|
|
941
|
+
```javascript
|
|
942
|
+
// Reference data (rarely changes)
|
|
943
|
+
const categories = db.Categories.toList();
|
|
944
|
+
const settings = db.Settings.toList();
|
|
945
|
+
|
|
946
|
+
// Read-heavy data (user profiles)
|
|
947
|
+
const user = db.User.findById(userId);
|
|
948
|
+
|
|
949
|
+
// Expensive aggregations
|
|
950
|
+
const stats = db.Orders
|
|
951
|
+
.where(o => o.status == $$, 'completed')
|
|
952
|
+
.count();
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
**DON'T cache:**
|
|
956
|
+
```javascript
|
|
957
|
+
// Real-time data (always needs fresh results)
|
|
958
|
+
const liveOrders = db.Orders
|
|
959
|
+
.where(o => o.status == $$, 'pending')
|
|
960
|
+
.noCache()
|
|
961
|
+
.toList();
|
|
962
|
+
|
|
963
|
+
// Financial transactions (critical accuracy)
|
|
964
|
+
const balance = db.Transactions
|
|
965
|
+
.where(t => t.user_id == $$, userId)
|
|
966
|
+
.noCache()
|
|
967
|
+
.toList();
|
|
968
|
+
|
|
969
|
+
// User-specific sensitive data (security concern)
|
|
970
|
+
const permissions = db.UserPermissions
|
|
971
|
+
.where(p => p.user_id == $$, userId)
|
|
972
|
+
.noCache()
|
|
973
|
+
.toList();
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
#### Monitoring Cache Performance
|
|
977
|
+
|
|
978
|
+
```javascript
|
|
979
|
+
// Log cache stats periodically
|
|
980
|
+
setInterval(() => {
|
|
981
|
+
const stats = db.getCacheStats();
|
|
982
|
+
console.log(`Cache: ${stats.hitRate} hit rate, ${stats.size}/${stats.maxSize} entries`);
|
|
983
|
+
}, 60000);
|
|
984
|
+
|
|
985
|
+
// Watch for low hit rates (< 50% might indicate poor cache strategy)
|
|
986
|
+
if (parseFloat(stats.hitRate) < 50) {
|
|
987
|
+
console.warn('Cache hit rate is low, consider tuning cache TTL or size');
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
757
991
|
### Multi-Context Applications
|
|
758
992
|
|
|
759
993
|
Manage multiple databases in one application:
|
|
@@ -827,6 +1061,11 @@ context.saveChanges() // MySQL/SQLite (sync)
|
|
|
827
1061
|
// Add/Remove entities
|
|
828
1062
|
context.EntityName.add(entity)
|
|
829
1063
|
context.remove(entity)
|
|
1064
|
+
|
|
1065
|
+
// Cache management
|
|
1066
|
+
context.getCacheStats() // Get cache statistics
|
|
1067
|
+
context.clearQueryCache() // Clear all cached queries
|
|
1068
|
+
context.setQueryCacheEnabled(bool) // Enable/disable caching
|
|
830
1069
|
```
|
|
831
1070
|
|
|
832
1071
|
### Query Methods
|
|
@@ -840,6 +1079,7 @@ context.remove(entity)
|
|
|
840
1079
|
.skip(number) // Skip N records
|
|
841
1080
|
.take(number) // Limit to N records
|
|
842
1081
|
.include(relationship) // Eager load
|
|
1082
|
+
.noCache() // Disable caching for this query
|
|
843
1083
|
|
|
844
1084
|
// Terminal methods (execute query)
|
|
845
1085
|
.toList() // Return array
|
|
@@ -1009,7 +1249,25 @@ console.log(`${author.name} has ${posts.length} posts`);
|
|
|
1009
1249
|
|
|
1010
1250
|
## Performance Tips
|
|
1011
1251
|
|
|
1012
|
-
### 1.
|
|
1252
|
+
### 1. Leverage Query Caching
|
|
1253
|
+
|
|
1254
|
+
```javascript
|
|
1255
|
+
// ā
GOOD: Cache reference data
|
|
1256
|
+
const categories = db.Categories.toList(); // Cached automatically
|
|
1257
|
+
|
|
1258
|
+
// ā
GOOD: Reuse queries (cache hits)
|
|
1259
|
+
const user1 = db.User.findById(123); // DB query
|
|
1260
|
+
const user2 = db.User.findById(123); // Cache hit (instant)
|
|
1261
|
+
|
|
1262
|
+
// ā
GOOD: Disable cache for real-time data
|
|
1263
|
+
const liveOrders = db.Orders.where(o => o.status == 'pending').noCache().toList();
|
|
1264
|
+
|
|
1265
|
+
// Monitor cache performance
|
|
1266
|
+
const stats = db.getCacheStats();
|
|
1267
|
+
console.log(`Cache hit rate: ${stats.hitRate}`); // Target: > 70%
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
### 2. Use Bulk Operations
|
|
1013
1271
|
|
|
1014
1272
|
```javascript
|
|
1015
1273
|
// ā BAD: Multiple inserts
|
|
@@ -1027,7 +1285,7 @@ for (const item of items) {
|
|
|
1027
1285
|
await db.saveChanges(); // Batch insert
|
|
1028
1286
|
```
|
|
1029
1287
|
|
|
1030
|
-
###
|
|
1288
|
+
### 3. Use Indexes
|
|
1031
1289
|
|
|
1032
1290
|
```javascript
|
|
1033
1291
|
class User {
|
|
@@ -1043,7 +1301,7 @@ class User {
|
|
|
1043
1301
|
// CREATE INDEX idx_user_status ON User(status);
|
|
1044
1302
|
```
|
|
1045
1303
|
|
|
1046
|
-
###
|
|
1304
|
+
### 4. Limit Result Sets
|
|
1047
1305
|
|
|
1048
1306
|
```javascript
|
|
1049
1307
|
// ā
GOOD: Limit results
|
|
@@ -1056,7 +1314,7 @@ const recentUsers = db.User
|
|
|
1056
1314
|
const allUsers = db.User.all();
|
|
1057
1315
|
```
|
|
1058
1316
|
|
|
1059
|
-
###
|
|
1317
|
+
### 5. Use Connection Pooling (PostgreSQL)
|
|
1060
1318
|
|
|
1061
1319
|
```javascript
|
|
1062
1320
|
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
|
+
}
|