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
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
+
}
|