sqlcache 2.9.0 → 2.9.1

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/docs/EXAMPLES.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  Comprehensive examples for common use cases.
4
4
 
5
+ Most examples omit cache names and invalidation prefixes, so they assume both automatic features are
6
+ on:
7
+
8
+ ```bash
9
+ # .env
10
+ CORE_AUTO_FEATURES=true # automatic cache keys: cacheName may be omitted
11
+ CORE_AUTO_INVALIDATION=true # automatic invalidation: writes without resetCacheName clear <table>:* and <table>_*
12
+ ```
13
+
14
+ Without `CORE_AUTO_FEATURES`, a call without a cache name throws `cacheName is required...`; without
15
+ `CORE_AUTO_INVALIDATION`, a write without `resetCacheName` clears nothing. The handlers are
16
+ Express-style, with `app`, `req` and `res` defined by your application.
17
+
5
18
  ---
6
19
 
7
20
  ## Table of Contents
@@ -41,11 +54,11 @@ app.post('/orders', async (req, res) => {
41
54
 
42
55
  // 2. Process each item
43
56
  for (const item of items) {
44
- // Check stock
45
- const [product] = await tx.getCacheQuery(
46
- 'SELECT id, name, price, stock FROM products WHERE id = ?',
47
- [item.productId],
48
- `product-${item.productId}`
57
+ // Check stock: read and lock the row inside the transaction, never from the
58
+ // cache (a cached stock level can be hours old)
59
+ const [product] = await tx.query(
60
+ 'SELECT id, name, price, stock FROM products WHERE id = ? FOR UPDATE',
61
+ [item.productId]
49
62
  );
50
63
 
51
64
  if (!product || product.stock < item.quantity) {
@@ -58,7 +71,7 @@ app.post('/orders', async (req, res) => {
58
71
  [orderId, item.productId, item.quantity, product.price]
59
72
  );
60
73
 
61
- // Update stock
74
+ // Update stock (products:* and products_* are cleared after the commit)
62
75
  await tx.query(
63
76
  'UPDATE products SET stock = stock - ? WHERE id = ?',
64
77
  [item.quantity, item.productId]
@@ -94,7 +107,9 @@ app.post('/orders', async (req, res) => {
94
107
  ### Product Catalog with Caching
95
108
 
96
109
  ```javascript
97
- // Get products by category (cached)
110
+ // Get products by category (cached under products:category_id:status:<hash>)
111
+ // Only the first table counts for keys and invalidation: when a category is renamed,
112
+ // pass 'products:' as resetCacheName so these listings are refreshed.
98
113
  app.get('/products/category/:categoryId', async (req, res) => {
99
114
  const { categoryId } = req.params;
100
115
  const { page = 1, limit = 20 } = req.query;
@@ -121,7 +136,7 @@ app.put('/products/:id', async (req, res) => {
121
136
  'UPDATE products SET name = ?, price = ?, stock = ?, category_id = ? WHERE id = ?',
122
137
  [name, price, stock, category_id, id]
123
138
  );
124
- // ✅ All product caches automatically cleared
139
+ // ✅ products:* and products_* automatically cleared (CORE_AUTO_INVALIDATION=true)
125
140
 
126
141
  res.json({ success: true });
127
142
  });
@@ -237,13 +252,18 @@ app.post('/posts/:id/like', async (req, res) => {
237
252
  ### Tenant Database Switching
238
253
 
239
254
  ```javascript
240
- // Middleware to get tenant database
255
+ const bcrypt = require('bcrypt'); // or another password-hashing library
256
+
257
+ // Middleware to get the tenant database: take the tenant from the authenticated session
258
+ // (set by your auth middleware) and check it against an allowlist, never from a raw header
259
+ const TENANT_DATABASES = new Set(['tenant_1', 'tenant_2', 'tenant_3']);
260
+
241
261
  const getTenantDb = (req, res, next) => {
242
- const tenantId = req.headers['x-tenant-id'];
243
- if (!tenantId) {
244
- return res.status(400).json({ error: 'Tenant ID required' });
262
+ const tenantDb = req.user && `tenant_${req.user.tenantId}`;
263
+ if (!tenantDb || !TENANT_DATABASES.has(tenantDb)) {
264
+ return res.status(403).json({ error: 'Unknown tenant' });
245
265
  }
246
- req.tenantDb = `tenant_${tenantId}`;
266
+ req.tenantDb = tenantDb;
247
267
  next();
248
268
  };
249
269
 
@@ -252,20 +272,23 @@ app.get('/users', getTenantDb, async (req, res) => {
252
272
  const users = await getCacheQuery(
253
273
  'SELECT id, name, email, role, created_at FROM users WHERE status = ?',
254
274
  ['active'],
255
- null, // Auto-generated cache key
275
+ null, // Auto-generated cache key, ends with @<tenant database>
256
276
  req.tenantDb // Tenant-specific database
257
277
  );
258
278
 
259
279
  res.json(users);
260
280
  });
261
281
 
262
- // Create user in tenant database
263
- app.post('/users', getTenantDb, async (req, res) => {
264
- const { name, email, password, role } = req.body;
282
+ // Create user in tenant database (requireAdmin: your own authorization middleware)
283
+ app.post('/users', getTenantDb, requireAdmin, async (req, res) => {
284
+ const { name, email, password } = req.body;
285
+ // Never take a role from the request as-is, and never store a plain-text password
286
+ const role = ['member', 'editor'].includes(req.body.role) ? req.body.role : 'member';
287
+ const passwordHash = await bcrypt.hash(password, 10);
265
288
 
266
289
  const result = await QuaryCache(
267
290
  'INSERT INTO users (name, email, password, role, created_at) VALUES (?, ?, ?, ?, NOW())',
268
- [name, email, password, role],
291
+ [name, email, passwordHash, role],
269
292
  null, // Auto-invalidation
270
293
  req.tenantDb
271
294
  );
@@ -278,21 +301,30 @@ app.post('/users', getTenantDb, async (req, res) => {
278
301
 
279
302
  ```javascript
280
303
  app.get('/dashboard/stats', getTenantDb, async (req, res) => {
304
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
305
+
281
306
  const stats = await withTransaction(async (tx) => {
282
- // Get multiple stats in parallel
307
+ // Several stats, one after another (a transaction runs on one connection).
308
+ // Aggregates get explicit cache names: an automatic key would be users:all@<db>, the same
309
+ // as for any other query on the table without a WHERE clause. Names under the
310
+ // '<table>:' prefix are still cleared by automatic invalidation.
283
311
  const [userStats] = await tx.getCacheQuery(
284
312
  'SELECT COUNT(*) as total, SUM(CASE WHEN status="active" THEN 1 ELSE 0 END) as active FROM users',
285
- []
313
+ [],
314
+ `users:stats@${req.tenantDb}`
286
315
  );
287
316
 
317
+ // The day is a parameter and part of the name, so yesterday's entry is not reused
288
318
  const [orderStats] = await tx.getCacheQuery(
289
- 'SELECT COUNT(*) as total, SUM(total_amount) as revenue FROM orders WHERE DATE(created_at) = CURDATE()',
290
- []
319
+ 'SELECT COUNT(*) as total, SUM(total_amount) as revenue FROM orders WHERE created_at >= ? AND created_at < ? + INTERVAL 1 DAY',
320
+ [today, today],
321
+ `orders:stats:${today}@${req.tenantDb}`
291
322
  );
292
323
 
293
324
  const [productStats] = await tx.getCacheQuery(
294
325
  'SELECT COUNT(*) as total, SUM(CASE WHEN stock < 10 THEN 1 ELSE 0 END) as low_stock FROM products',
295
- []
326
+ [],
327
+ `products:stats@${req.tenantDb}`
296
328
  );
297
329
 
298
330
  return {
@@ -319,9 +351,12 @@ const { bulkInsert } = require('sqlcache');
319
351
  app.post('/analytics/events/bulk', async (req, res) => {
320
352
  const events = req.body.events; // Array of events
321
353
 
322
- // Add timestamps
354
+ // Map each event onto a fixed set of columns: bulkInsert uses the keys of the first
355
+ // record as column names, so never pass request objects (or spread them) as they are
323
356
  const enrichedEvents = events.map(event => ({
324
- ...event,
357
+ event_type: String(event.type),
358
+ user_id: event.userId,
359
+ data: JSON.stringify(event.data),
325
360
  created_at: new Date(),
326
361
  ip_address: req.ip
327
362
  }));
@@ -459,6 +494,9 @@ app.post('/chat/rooms/:roomId/read', async (req, res) => {
459
494
  ### User Registration with Transaction
460
495
 
461
496
  ```javascript
497
+ const bcrypt = require('bcrypt'); // or another password-hashing library
498
+ const { withTransaction } = require('sqlcache');
499
+
462
500
  app.post('/auth/register', async (req, res) => {
463
501
  const { email, password, name } = req.body;
464
502
 
@@ -508,16 +546,19 @@ app.post('/auth/register', async (req, res) => {
508
546
  ### Session Management
509
547
 
510
548
  ```javascript
511
- const { addArrayItem, getArrayItem, delKeyItem } = require('sqlcache');
549
+ const crypto = require('crypto');
550
+ const bcrypt = require('bcrypt');
551
+ const { QuaryCache, addArrayItem, getArrayItem, delKeyItem } = require('sqlcache');
512
552
 
513
553
  // Create session
514
554
  app.post('/auth/login', async (req, res) => {
515
555
  const { email, password } = req.body;
516
556
 
517
- const [user] = await getCacheQuery(
557
+ // Read credentials from MySQL, not through the cache: a cached password hash would keep an
558
+ // old password valid until the entry expires (QuaryCache neither reads the cache nor stores results)
559
+ const [user] = await QuaryCache(
518
560
  'SELECT id, email, password, name FROM users WHERE email = ?',
519
- [email],
520
- `user-email-${email}`
561
+ [email]
521
562
  );
522
563
 
523
564
  if (!user || !(await bcrypt.compare(password, user.password))) {
package/docs/MIGRATION.md CHANGED
@@ -1,6 +1,83 @@
1
1
  # Migration Guide
2
2
 
3
- Guide for migrating to v2.6.0 and adopting smart features.
3
+ Guide for upgrading between sqlcache versions and adopting the automatic features.
4
+
5
+ ---
6
+
7
+ ## v2.9.0 → v2.9.1
8
+
9
+ A patch release. The API is unchanged; some TypeScript types accept more than before. The
10
+ differences:
11
+
12
+ ### Requirements
13
+
14
+ - Node.js 14 or newer (tested on 18–26); `engines.node` is `>=14`.
15
+ - mysql2 3.23.1 or newer (the dependency is now `^3.23.1`). Older versions have security
16
+ advisories, including critical ones; update lockfiles that pin an older mysql2.
17
+
18
+ ### Imports
19
+
20
+ - Named ESM imports work: `import { getCacheQuery } from 'sqlcache'`. On 2.9.0 and earlier, native
21
+ ESM could only import `QuaryCache` by name; use the default import there.
22
+ - `import sqlcache from 'sqlcache'` works without `esModuleInterop`: the module now has a
23
+ non-enumerable `default` property, as the type definitions always declared.
24
+ - Types: `QuaryCache<T>()` takes the result type (default `QueryResult`, which gained `info` and
25
+ `warningStatus`); `resetCacheName` accepts `string[]` in `QuaryCache` and `bulkInsert`; `parameters`
26
+ is optional except in `getCacheQueryPagination`; the cache name may be omitted in
27
+ `getCacheQueryWithTimeout` and passed as `null` to `getCacheQueryPagination`; a `withTransaction`
28
+ callback may return a plain value; `configure`, `enableAutoKey` and `enableAutoInvalidation` accept
29
+ booleans.
30
+
31
+ ### Caching
32
+
33
+ - Empty results are no longer written to the cache; they were never served from it.
34
+ - A cached value that is not a non-empty array is treated as a miss. For pagination, it must be a
35
+ page object.
36
+ - A read that started before an invalidation in the same process no longer writes its possibly stale
37
+ result to the cache. Across processes a short window remains.
38
+ - `tx.getCacheQuery` still reads the cache first, but writes to it only after the transaction commits.
39
+ Nothing is cached from a rolled-back transaction.
40
+ - `getCacheQueryPagination` and `getCacheQueryWithTimeout` without a cache name use an automatic key
41
+ when automatic keys are enabled; otherwise the result is not cached and a one-time warning is
42
+ logged. Before 2.9.1 all such calls shared one cache key.
43
+ - Automatic keys of queries without a detectable table (`CALL`, `SELECT fn(?)`) now include the
44
+ parameters: `query:<sqlhash>:<paramhash>`. `BigInt` parameters no longer throw.
45
+ - New `DB_COALESCE_MAX_WAIT` (ms, default 10000): how long concurrent cache misses wait for a shared
46
+ query before starting a fresh one, which protects against a stuck connection. A waiting caller
47
+ still takes the original result if it arrives first. When a shared query fails with
48
+ `ER_QUERY_TIMEOUT` (`MAX_EXECUTION_TIME`), that error goes to every caller waiting on it.
49
+
50
+ ### Turning features off
51
+
52
+ - `enableAutoKey(false)`, `enableAutoInvalidation(false)` and `configure({ autoKey: false })` now
53
+ disable the feature. Before 2.9.1 they enabled it; check calls that pass `false`.
54
+
55
+ ### bulkInsert
56
+
57
+ - Invalid table or column names are rejected before anything is sent, with a `TypeError`:
58
+ `bulkInsert: invalid table name "<name>"` or `bulkInsert: invalid column name "<name>"`. Valid names
59
+ are unchanged.
60
+ - Auto-invalidation (`CORE_AUTO_INVALIDATION` / `enableAutoInvalidation`) now also applies to
61
+ `bulkInsert` when no `resetCacheName` is given.
62
+ - Invalidation also runs when `bulkInsert` fails after rows may have been written.
63
+ - With `transaction: true`, an error from `COMMIT` is not retried and carries
64
+ `err.commitOutcomeUnknown = true`.
65
+
66
+ ### Queries and connections
67
+
68
+ - `cancelOnTimeout`: the connection whose query was killed is discarded instead of being returned to
69
+ the pool, at most 4 cancellations run at once, and a timeout during the database switch also stops
70
+ the query from being sent.
71
+ - Pagination appends `LIMIT` on a new line, so SQL ending in a `-- comment` paginates correctly.
72
+ - `DROP DATABASE` / `DROP SCHEMA` in your SQL is handled like `USE`: the next call on that pooled
73
+ connection selects its database again.
74
+
75
+ ### Redis
76
+
77
+ - `isRedisConnected()` returns `false` without opening a connection when `REDIS_ENABLED=false`.
78
+ - One log line per reconnect attempt: `Redis reconnecting... (attempt N, delay Xms)`.
79
+ - A log line appears if more than 1000 invalidations are missed during an outage.
80
+ - A one-time warning appears if `REDIS_VHOST` changes at runtime.
4
81
 
5
82
  ---
6
83
 
@@ -52,9 +129,10 @@ app.post('/users', async (req, res) => {
52
129
  const { getCacheQuery, QuaryCache } = require('sqlcache');
53
130
 
54
131
  // .env
55
- CORE_AUTO_FEATURES=true
132
+ CORE_AUTO_FEATURES=true // automatic cache keys
133
+ CORE_AUTO_INVALIDATION=true // automatic invalidation (CORE_AUTO_FEATURES alone does not enable it)
56
134
 
57
- // Read with auto-cache
135
+ // Read with auto-cache (key users:id:<hash>)
58
136
  app.get('/users/:id', async (req, res) => {
59
137
  const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [req.params.id]);
60
138
  res.json(users[0]);
@@ -63,16 +141,19 @@ app.get('/users/:id', async (req, res) => {
63
141
  // Write with auto-invalidation
64
142
  app.post('/users', async (req, res) => {
65
143
  await QuaryCache('INSERT INTO users (name, email) VALUES (?, ?)', [req.body.name, req.body.email]);
66
- // ✅ All user caches automatically cleared!
144
+ // ✅ users:* and users_* automatically cleared
67
145
  res.json({ success: true });
68
146
  });
69
147
  ```
70
148
 
71
149
  **Benefits:**
72
150
  - 10+ lines → 3 lines
73
- - No cache key management
74
- - No manual invalidation
75
- - No forgotten keys
151
+ - No cache key management for simple queries
152
+ - No manual invalidation for writes to the cached table
153
+
154
+ Automatic keys and invalidation follow the first table of each statement only; see the README's
155
+ [Cache keys and invalidation](../README.md#cache-keys-and-invalidation) for what needs an explicit
156
+ cache name or `resetCacheName` (joins, aggregates, `CALL`, writes from other applications, ...).
76
157
 
77
158
  ---
78
159
 
@@ -95,8 +176,8 @@ const [users] = await pool.query('SELECT * FROM users WHERE id = ?', [123]);
95
176
  // After
96
177
  const { getCacheQuery } = require('sqlcache');
97
178
 
98
- const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123]);
99
- // ✅ Same API + automatic caching!
179
+ const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123], 'user:123');
180
+ // ✅ Same SQL and parameters + automatic caching; returns the rows directly (no [rows, fields])
100
181
  ```
101
182
 
102
183
  ### From `knex`
@@ -118,13 +199,15 @@ const users = await knex('users').where('id', 123).select('*');
118
199
  // After
119
200
  const { getCacheQuery } = require('sqlcache');
120
201
 
121
- const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123]);
202
+ const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123], 'user:123');
122
203
  ```
123
204
 
124
205
  ---
125
206
 
126
207
  ## Version Migration
127
208
 
209
+ For v2.9.0 → v2.9.1 see [the top of this guide](#v290--v291).
210
+
128
211
  ### Package renamed to `sqlcache` (v2.9.0)
129
212
 
130
213
  From v2.9.0 the package is published as `sqlcache`; the API is unchanged.
@@ -144,7 +227,8 @@ const { getCacheQuery } = require('sqlcache');
144
227
  Deep imports (`/dbConnector`, `/redis.Connector`, `/core/...`) and TypeScript imports change the same
145
228
  way. You do not have to switch right away: `node-caching-mysql-connector-with-redis@2.9.0` only
146
229
  forwards to `sqlcache@^2.9.0` (including those deep paths and the types), so `npm update` on the old
147
- name already runs the same code. The old name is deprecated and gets no new major versions.
230
+ name already runs the same code. The old name is deprecated and gets no new major versions. Deep
231
+ imports have no type declarations of their own; TypeScript code should import from `'sqlcache'`.
148
232
 
149
233
  ### v2.8.x → v2.9.0 (performance & resilience)
150
234
 
@@ -181,14 +265,17 @@ can be switched back with an environment variable.
181
265
  they write to (previously no table, or a wrong one).
182
266
  - **`bulkInsert` no longer re-inserts committed chunks** when a connection error triggers a retry.
183
267
  - **`closeConnections()` no longer hangs the process.** With mysql2 3.3.x the pool's idle reaper
184
- kept running after `end()` and later spun in a synchronous loop at 100% CPU.
268
+ kept running after `end()` and later spun in a synchronous loop at 100% CPU. It closes only the
269
+ MySQL pool: with Redis enabled, also call `getRedisClient().quit()`, or the open Redis connection
270
+ keeps the process alive.
185
271
  - **The Redis client really never stops reconnecting.** node-redis gave up after one hour of retries,
186
272
  leaving the cache disabled until a restart.
187
273
  - **Invalidations missed during a Redis outage are replayed** when Redis is ready again, instead of
188
274
  leaving stale entries for the rest of their TTL.
189
275
  - A failed command sent without a callback through `getRedisClient()` no longer marks Redis as down.
190
276
  - `REDIS_ENABLED=false` no longer opens a Redis connection (and reconnect loop) at startup; it is
191
- opened on the first direct Redis call, including `isRedisConnected()`.
277
+ opened on the first direct Redis call, including `isRedisConnected()` (no longer the case for
278
+ `isRedisConnected()` since v2.9.1).
192
279
  - An empty `REDIS_PASSWORD` no longer sends `AUTH ""`.
193
280
 
194
281
  #### Smaller observable differences (no switch)
@@ -224,6 +311,8 @@ return the same results with less work.
224
311
  ### v2.5.x → v2.6.0
225
312
 
226
313
  #### What's New
314
+ - Automatic cache keys and invalidation, opt-in (`CORE_AUTO_FEATURES`, `CORE_AUTO_INVALIDATION`,
315
+ `configure()`, `enableAutoKey()`, `enableAutoInvalidation()`)
227
316
  - Transaction support (`withTransaction`)
228
317
  - Enhanced documentation
229
318
  - More comprehensive tests
@@ -241,7 +330,7 @@ return the same results with less work.
241
330
  2. **No code changes required**
242
331
  ```javascript
243
332
  // Your v2.5.x code works exactly the same
244
- await getCacheQuery('SELECT * FROM users', [], 'users-all');
333
+ await getCacheQuery('SELECT * FROM users', [], 'users_all');
245
334
  await QuaryCache('INSERT INTO users...', [data], 'users_*');
246
335
  ```
247
336
 
@@ -256,6 +345,7 @@ return the same results with less work.
256
345
  await connection.commit();
257
346
  } catch (error) {
258
347
  await connection.rollback();
348
+ throw error;
259
349
  } finally {
260
350
  connection.release();
261
351
  }
@@ -280,21 +370,27 @@ npm install sqlcache
280
370
  // Use with manual keys (no changes to existing logic)
281
371
  const { getCacheQuery, QuaryCache } = require('sqlcache');
282
372
 
283
- await getCacheQuery('SELECT * FROM users', [], 'users-all');
284
- await QuaryCache('INSERT INTO users...', [data], 'users_*');
373
+ await getCacheQuery('SELECT * FROM users', [], 'users_all');
374
+ await QuaryCache('INSERT INTO users...', [data], 'users_*'); // clears users_all
285
375
  ```
286
376
 
287
377
  ### Phase 2: Enable Auto Features
288
378
 
289
379
  ```javascript
290
380
  // .env
291
- CORE_AUTO_FEATURES=true
381
+ CORE_AUTO_FEATURES=true // automatic keys
382
+ CORE_AUTO_INVALIDATION=true // automatic invalidation (not enabled by CORE_AUTO_FEATURES)
292
383
 
293
384
  // Remove cache keys from new code
294
- await getCacheQuery('SELECT * FROM users', []); // Auto key ✨
385
+ await getCacheQuery('SELECT * FROM users', []); // Auto key users:all ✨
295
386
 
296
387
  // Old code with manual keys still works
297
- await getCacheQuery('SELECT * FROM orders', [], 'orders-all'); // Manual key
388
+ await getCacheQuery('SELECT * FROM orders', [], 'orders_all'); // Manual key
389
+
390
+ // But a resetCacheName replaces the automatic patterns, so old writes that clear only
391
+ // 'users_*' leave the new users:... keys stale. Drop the resetCacheName from such writes,
392
+ // or clear both prefixes:
393
+ await QuaryCache('INSERT INTO users...', [data], ['users_', 'users:']);
298
394
  ```
299
395
 
300
396
  ### Phase 3: Adopt Transactions
@@ -312,14 +408,17 @@ await withTransaction(async (tx) => {
312
408
 
313
409
  ```javascript
314
410
  // Before
315
- await getCacheQuery('SELECT * FROM users', [], 'users-all');
316
- await getCacheQuery('SELECT * FROM posts', [], 'posts-recent');
411
+ await getCacheQuery('SELECT * FROM users', [], 'users_all');
412
+ await getCacheQuery('SELECT * FROM posts', [], 'posts_recent');
317
413
 
318
414
  // After (once confident in auto features)
319
415
  await getCacheQuery('SELECT * FROM users', []);
320
416
  await getCacheQuery('SELECT * FROM posts', []);
321
417
  ```
322
418
 
419
+ Keep explicit names where automatic keys would collide, such as aggregates, joins and
420
+ `LIMIT`/`ORDER BY` variants (see the README's caveats).
421
+
323
422
  ---
324
423
 
325
424
  ## Common Migration Scenarios
@@ -342,7 +441,7 @@ app.get('/users', async (req, res) => {
342
441
  const { getCacheQuery } = require('sqlcache');
343
442
 
344
443
  app.get('/users', async (req, res) => {
345
- const users = await getCacheQuery('SELECT * FROM users', []);
444
+ const users = await getCacheQuery('SELECT * FROM users', []); // auto key users:all (CORE_AUTO_FEATURES=true)
346
445
  res.json(users);
347
446
  });
348
447
  ```
@@ -365,11 +464,12 @@ async function processOrders() {
365
464
  const { QuaryCache } = require('sqlcache');
366
465
 
367
466
  async function processOrders() {
368
- const orders = await getCacheQuery('SELECT * FROM orders WHERE status = ?', ['pending']);
467
+ // A work queue must see the current rows: QuaryCache reads MySQL without the cache
468
+ const orders = await QuaryCache('SELECT * FROM orders WHERE status = ?', ['pending']);
369
469
 
370
470
  for (const order of orders) {
371
471
  await QuaryCache('UPDATE orders SET status = ? WHERE id = ?', ['processing', order.id]);
372
- // ✅ Caches automatically cleared
472
+ // ✅ orders:* and orders_* cleared (CORE_AUTO_INVALIDATION=true)
373
473
  }
374
474
  }
375
475
  ```
@@ -408,7 +508,8 @@ async function createUserWithProfile(userData, profileData) {
408
508
  const userResult = await tx.query('INSERT INTO users...', [userData]);
409
509
  await tx.query('INSERT INTO profiles...', [userResult.insertId, profileData]);
410
510
  return userResult.insertId;
411
- // ✅ Auto commit, auto rollback, auto cache invalidation
511
+ // ✅ Auto commit, auto rollback, cache invalidation after the commit
512
+ // (automatic with CORE_AUTO_INVALIDATION=true, or pass resetCacheName to tx.query)
412
513
  });
413
514
  }
414
515
  ```
@@ -433,7 +534,7 @@ const { getCacheQuery } = require('sqlcache');
433
534
 
434
535
  describe('User API', () => {
435
536
  it('should fetch users with cache', async () => {
436
- const users = await getCacheQuery('SELECT * FROM users', []);
537
+ const users = await getCacheQuery('SELECT * FROM users', [], 'users_all');
437
538
  expect(users).to.be.an('array');
438
539
  });
439
540
  });
@@ -441,12 +542,16 @@ describe('User API', () => {
441
542
 
442
543
  ### Integration Tests
443
544
 
545
+ `REDIS_ENABLED`, like the pool and coalescing settings, is read once, when sqlcache is first
546
+ required. Changing it afterwards has no effect, so set it before the first `require` and run the
547
+ Redis and no-Redis variants as separate test processes:
548
+
444
549
  ```javascript
445
- // Test with Redis enabled
550
+ // test/setup-redis.js (one test run)
446
551
  process.env.REDIS_ENABLED = 'true';
447
552
  process.env.CORE_AUTO_FEATURES = 'true';
448
553
 
449
- // Test without Redis (fallback)
554
+ // test/setup-no-redis.js (another test run): MySQL only, no caching
450
555
  process.env.REDIS_ENABLED = 'false';
451
556
  ```
452
557
 
@@ -459,11 +564,12 @@ If you need to rollback:
459
564
  1. **Disable auto features**
460
565
  ```bash
461
566
  CORE_AUTO_FEATURES=false
567
+ CORE_AUTO_INVALIDATION=false
462
568
  ```
463
569
 
464
570
  2. **Add manual keys back**
465
571
  ```javascript
466
- await getCacheQuery('SELECT * FROM users', [], 'users-all');
572
+ await getCacheQuery('SELECT * FROM users', [], 'users_all');
467
573
  await QuaryCache('INSERT INTO users...', [data], 'users_*');
468
574
  ```
469
575
 
@@ -478,23 +584,17 @@ If you need to rollback:
478
584
 
479
585
  ### Before Migration
480
586
 
481
- - Manual cache management: ~50ms per request
482
587
  - Risk of stale data due to missed invalidations
483
588
  - Complex code maintenance
484
589
 
485
590
  ### After Migration
486
591
 
487
- - Auto cache: ~2ms overhead
488
- - Guaranteed cache consistency
489
- - Simpler code (80% less boilerplate)
490
-
491
- ### Benchmark
492
-
493
- ```javascript
494
- // Manual approach: 10 lines, 50ms
495
- // Auto approach: 2 lines, 52ms
496
- // Trade-off: +2ms for huge DX improvement ✅
497
- ```
592
+ - Cache hits are served from Redis; a miss adds a Redis read to the query, and a Redis write when
593
+ the result is not empty
594
+ - Invalidation is automatic for writes made through sqlcache to the first table of each statement;
595
+ joins, aggregates, `CALL`, writes from other applications and the cases listed in the README still
596
+ need explicit cache names or `resetCacheName`
597
+ - Simpler code
498
598
 
499
599
  ---
500
600
 
@@ -502,7 +602,6 @@ If you need to rollback:
502
602
 
503
603
  - **Documentation**: [API Reference](API.md) | [Examples](EXAMPLES.md)
504
604
  - **Issues**: [GitHub Issues](https://github.com/hayatialikeles/sqlcache/issues)
505
- - **Community**: [Discussions](https://github.com/hayatialikeles/sqlcache/discussions)
506
605
 
507
606
  ---
508
607