forge-sql-orm 2.0.30 → 2.1.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.
Files changed (46) hide show
  1. package/README.md +1410 -81
  2. package/dist/ForgeSQLORM.js +1456 -60
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +1440 -61
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLAnalyseOperations.d.ts +1 -1
  7. package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLCacheOperations.d.ts +119 -0
  9. package/dist/core/ForgeSQLCacheOperations.d.ts.map +1 -0
  10. package/dist/core/ForgeSQLCrudOperations.d.ts +38 -22
  11. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  12. package/dist/core/ForgeSQLORM.d.ts +248 -13
  13. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  14. package/dist/core/ForgeSQLQueryBuilder.d.ts +394 -19
  15. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/lib/drizzle/extensions/additionalActions.d.ts +90 -0
  19. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -0
  20. package/dist/utils/cacheContextUtils.d.ts +123 -0
  21. package/dist/utils/cacheContextUtils.d.ts.map +1 -0
  22. package/dist/utils/cacheUtils.d.ts +56 -0
  23. package/dist/utils/cacheUtils.d.ts.map +1 -0
  24. package/dist/utils/sqlUtils.d.ts +8 -0
  25. package/dist/utils/sqlUtils.d.ts.map +1 -1
  26. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +46 -0
  27. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -0
  28. package/dist/webtriggers/index.d.ts +1 -0
  29. package/dist/webtriggers/index.d.ts.map +1 -1
  30. package/package.json +15 -12
  31. package/src/core/ForgeSQLAnalyseOperations.ts +1 -1
  32. package/src/core/ForgeSQLCacheOperations.ts +195 -0
  33. package/src/core/ForgeSQLCrudOperations.ts +49 -40
  34. package/src/core/ForgeSQLORM.ts +743 -34
  35. package/src/core/ForgeSQLQueryBuilder.ts +456 -20
  36. package/src/index.ts +1 -1
  37. package/src/lib/drizzle/extensions/additionalActions.ts +852 -0
  38. package/src/lib/drizzle/extensions/types.d.ts +99 -10
  39. package/src/utils/cacheContextUtils.ts +212 -0
  40. package/src/utils/cacheUtils.ts +403 -0
  41. package/src/utils/sqlUtils.ts +42 -0
  42. package/src/webtriggers/clearCacheSchedulerTrigger.ts +79 -0
  43. package/src/webtriggers/index.ts +1 -0
  44. package/dist/lib/drizzle/extensions/selectAliased.d.ts +0 -9
  45. package/dist/lib/drizzle/extensions/selectAliased.d.ts.map +0 -1
  46. package/src/lib/drizzle/extensions/selectAliased.ts +0 -72
package/README.md CHANGED
@@ -17,8 +17,13 @@
17
17
 
18
18
  ## Key Features
19
19
  - ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
20
+ - ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
21
+ - ✅ **Global Cache System (Level 2)** with cross-invocation caching, automatic cache invalidation and context-aware operations (using [@forge/kvs](https://developer.atlassian.com/platform/forge/storage-reference/storage-api-custom-entities/) )
20
22
  - ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
21
23
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
24
+ - ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
25
+ - ✅ **Raw SQL Execution**: `execute()` and `executeCacheable()` methods for direct SQL queries with local and global caching
26
+ - ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
22
27
  - ✅ **Schema migration support**, allowing automatic schema evolution
23
28
  - ✅ **Automatic entity generation** from MySQL/tidb databases
24
29
  - ✅ **Automatic migration generation** from MySQL/tidb databases
@@ -26,24 +31,129 @@
26
31
  - ✅ **Schema Fetching** Development-only web trigger to retrieve current database schema and generate SQL statements for schema recreation
27
32
  - ✅ **Ready-to-use Migration Triggers** Built-in web triggers for applying migrations, dropping tables (development-only), and fetching schema (development-only) with proper error handling and security controls
28
33
  - ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
29
- - ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights (Performance analysis and Troubleshooting only)
34
+ - ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
35
+
36
+ ## Table of Contents
37
+
38
+ ### 🚀 Getting Started
39
+ - [Key Features](#key-features)
40
+ - [Usage Approaches](#usage-approaches)
41
+ - [Installation](#installation)
42
+ - [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
43
+ - [Quick Start](#quick-start)
44
+
45
+ ### 📖 Core Features
46
+ - [Field Name Collision Prevention](#field-name-collision-prevention-in-complex-queries)
47
+ - [Drizzle Usage with forge-sql-orm](#drizzle-usage-with-forge-sql-orm)
48
+ - [Direct Drizzle Usage with Custom Driver](#direct-drizzle-usage-with-custom-driver)
49
+
50
+ ### 🗄️ Database Operations
51
+ - [Fetch Data](#fetch-data)
52
+ - [Modify Operations](#modify-operations)
53
+ - [SQL Utilities](#sql-utilities)
54
+
55
+ ### ⚡ Caching System
56
+ - [Setting Up Caching with @forge/kvs](#setting-up-caching-with-forgekvs-optional)
57
+ - [Global Cache System (Level 2)](#global-cache-system-level-2)
58
+ - [Cache Context Operations](#cache-context-operations)
59
+ - [Local Cache Operations (Level 1)](#local-cache-operations-level-1)
60
+ - [Cache-Aware Query Operations](#cache-aware-query-operations)
61
+ - [Manual Cache Management](#manual-cache-management)
62
+
63
+ ### 🔒 Advanced Features
64
+ - [Optimistic Locking](#optimistic-locking)
65
+ - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
66
+ - [Date and Time Types](#date-and-time-types)
67
+
68
+ ### 🛠️ Development Tools
69
+ - [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
70
+ - [Web Triggers for Migrations](#web-triggers-for-migrations)
71
+ - [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
72
+ - [Drop Migrations](#drop-migrations)
73
+
74
+ ### 📚 Examples
75
+ - [Simple Example](examples/forge-sql-orm-example-simple)
76
+ - [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
77
+ - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
78
+ - [Dynamic Queries Example](examples/forge-sql-orm-example-dynamic)
79
+ - [Query Analysis Example](examples/forge-sql-orm-example-query-analyses)
80
+ - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
81
+ - [Checklist Example](examples/forge-sql-orm-example-checklist)
82
+
83
+ ### 📚 Reference
84
+ - [ForgeSqlOrmOptions](#forgesqlormoptions)
85
+ - [Migration Guide](#migration-guide)
86
+
87
+ ## 🚀 Quick Navigation
88
+
89
+ **New to Forge-SQL-ORM?** Start here:
90
+ - [Quick Start](#quick-start) - Get up and running in 5 minutes
91
+ - [Installation](#installation) - Complete setup guide
92
+ - [Basic Usage Examples](#fetch-data) - Simple query examples
93
+
94
+ **Looking for specific features?**
95
+ - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
96
+ - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
97
+ - [Optimistic Locking](#optimistic-locking) - Data consistency
98
+ - [Migration Tools](#web-triggers-for-migrations) - Database migrations
99
+ - [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
100
+
101
+ **Looking for practical examples?**
102
+ - [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
103
+ - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
104
+ - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
105
+ - [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
30
106
 
31
107
  ## Usage Approaches
32
108
 
33
- ### 1. Direct Drizzle Usage
109
+
110
+ ### 1. Full Forge-SQL-ORM Usage
111
+ ```typescript
112
+ import ForgeSQL from "forge-sql-orm";
113
+ const forgeSQL = new ForgeSQL();
114
+ ```
115
+ Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
116
+
117
+ ### 2. Direct Drizzle Usage
34
118
  ```typescript
35
119
  import { drizzle } from "drizzle-orm/mysql-proxy";
36
120
  import { forgeDriver } from "forge-sql-orm";
37
121
  const db = drizzle(forgeDriver);
38
122
  ```
39
- Best for: Simple CRUD operations without optimistic locking. Note that you need to manually patch drizzle `patchDbWithSelectAliased` for select fields to prevent field name collisions in Atlassian Forge SQL.
123
+ Best for: Simple Modify operations without optimistic locking. Note that you need to manually patch drizzle `patchDbWithSelectAliased` for select fields to prevent field name collisions in Atlassian Forge SQL.
40
124
 
41
- ### 2. Full Forge-SQL-ORM Usage
125
+
126
+ ### 3. Local Cache Optimization
42
127
  ```typescript
43
128
  import ForgeSQL from "forge-sql-orm";
44
129
  const forgeSQL = new ForgeSQL();
130
+
131
+ // Optimize repeated queries within a single invocation
132
+ await forgeSQL.executeWithLocalContext(async () => {
133
+ // Multiple queries here will benefit from local caching
134
+ const users = await forgeSQL.select({ id: users.id, name: users.name })
135
+ .from(users).where(eq(users.active, true));
136
+
137
+ // This query will use local cache (no database call)
138
+ const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
139
+ .from(users).where(eq(users.active, true));
140
+
141
+ // Using new methods for better performance
142
+ const usersFrom = await forgeSQL.selectFrom(users)
143
+ .where(eq(users.active, true));
144
+
145
+ // This will use local cache (no database call)
146
+ const cachedUsersFrom = await forgeSQL.selectFrom(users)
147
+ .where(eq(users.active, true));
148
+
149
+ // Raw SQL with local caching
150
+ const rawUsers = await forgeSQL.execute(
151
+ "SELECT id, name FROM users WHERE active = ?",
152
+ [true]
153
+ );
154
+ });
45
155
  ```
46
- Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
156
+ Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
47
157
 
48
158
  ## Field Name Collision Prevention in Complex Queries
49
159
 
@@ -91,18 +201,149 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
91
201
 
92
202
  ✅ Step 1: Install Dependencies
93
203
 
204
+ **Basic installation (without caching):**
94
205
  ```sh
95
- npm install forge-sql-orm @forge/sql drizzle-orm momment -S
96
- npm install forge-sql-orm-cli -D
206
+ npm install forge-sql-orm @forge/sql drizzle-orm -S
207
+ ```
208
+
209
+ **With caching support:**
210
+ ```sh
211
+ npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
97
212
  ```
98
213
 
99
214
  This will:
100
215
  - Install Forge-SQL-ORM (the ORM for @forge/sql)
101
216
  - Install @forge/sql, the Forge database layer
217
+ - Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
102
218
  - Install Drizzle ORM and its MySQL driver
103
219
  - Install TypeScript types for MySQL
104
220
  - Install forge-sql-orm-cli A command-line interface tool for managing Atlassian Forge SQL migrations and model generation with Drizzle ORM integration.
105
221
 
222
+ ## Quick Start
223
+
224
+ ### 1. Basic Setup
225
+ ```typescript
226
+ import ForgeSQL from "forge-sql-orm";
227
+
228
+ // Initialize ForgeSQL
229
+ const forgeSQL = new ForgeSQL();
230
+
231
+ // Simple query
232
+ const users = await forgeSQL.select().from(users);
233
+ ```
234
+
235
+ ### 2. With Caching (Optional)
236
+ ```typescript
237
+ import ForgeSQL from "forge-sql-orm";
238
+
239
+ // Initialize with caching
240
+ const forgeSQL = new ForgeSQL({
241
+ cacheEntityName: "cache",
242
+ cacheTTL: 300
243
+ });
244
+
245
+ // Cached query
246
+ const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
247
+ .from(users).where(eq(users.active, true));
248
+ ```
249
+
250
+ ### 3. Local Cache Optimization
251
+ ```typescript
252
+ // Optimize repeated queries within a single invocation
253
+ await forgeSQL.executeWithLocalContext(async () => {
254
+ const users = await forgeSQL.select({ id: users.id, name: users.name })
255
+ .from(users).where(eq(users.active, true));
256
+
257
+ // This query will use local cache (no database call)
258
+ const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
259
+ .from(users).where(eq(users.active, true));
260
+
261
+ // Using new methods for better performance
262
+ const usersFrom = await forgeSQL.selectFrom(users)
263
+ .where(eq(users.active, true));
264
+
265
+ // Raw SQL with local caching
266
+ const rawUsers = await forgeSQL.execute(
267
+ "SELECT id, name FROM users WHERE active = ?",
268
+ [true]
269
+ );
270
+ });
271
+ ```
272
+
273
+ ### 4. Next Steps
274
+ - [Full Installation Guide](#installation) - Complete setup instructions
275
+ - [Core Features](#core-features) - Learn about key capabilities
276
+ - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
277
+ - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory caching features
278
+ - [API Reference](#reference) - Complete API documentation
279
+
280
+ ## Drizzle Usage with forge-sql-orm
281
+
282
+ If you prefer to use Drizzle ORM with the additional features of Forge-SQL-ORM (like optimistic locking and caching), you can use the enhanced API:
283
+
284
+ ```typescript
285
+ import ForgeSQL from "forge-sql-orm";
286
+ const forgeSQL = new ForgeSQL();
287
+
288
+ // Versioned operations with cache management (recommended)
289
+ await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
290
+ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(updateData, Users);
291
+
292
+ // Versioned operations without cache management
293
+ await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
294
+ await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
295
+
296
+ // Non-versioned operations with cache management
297
+ await forgeSQL.insertAndEvictCache(Users).values(userData);
298
+ await forgeSQL.updateAndEvictCache(Users).set(updateData).where(eq(Users.id, 1));
299
+
300
+ // Basic Drizzle operations (cache context aware)
301
+ await forgeSQL.insert(Users).values(userData);
302
+ await forgeSQL.update(Users).set(updateData).where(eq(Users.id, 1));
303
+
304
+ // Direct Drizzle access
305
+ const db = forgeSQL.getDrizzleQueryBuilder();
306
+ const users = await db.select().from(users);
307
+
308
+ // Using new methods for enhanced functionality
309
+ const usersFrom = await forgeSQL.selectFrom(users)
310
+ .where(eq(users.active, true));
311
+
312
+ const usersDistinct = await forgeSQL.selectDistinctFrom(users)
313
+ .where(eq(users.active, true));
314
+
315
+ const usersCacheable = await forgeSQL.selectCacheableFrom(users)
316
+ .where(eq(users.active, true));
317
+
318
+ // Raw SQL execution
319
+ const rawUsers = await forgeSQL.execute(
320
+ "SELECT * FROM users WHERE active = ?",
321
+ [true]
322
+ );
323
+
324
+ // Raw SQL with caching
325
+ const cachedRawUsers = await forgeSQL.executeCacheable(
326
+ "SELECT * FROM users WHERE active = ?",
327
+ [true],
328
+ 300
329
+ );
330
+
331
+ // Common Table Expressions (CTEs)
332
+ const userStats = await forgeSQL
333
+ .with(
334
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
335
+ forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
336
+ )
337
+ .select({
338
+ totalActiveUsers: sql`COUNT(au.id)`,
339
+ totalCompletedOrders: sql`COUNT(co.id)`
340
+ })
341
+ .from(sql`activeUsers au`)
342
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
343
+ ```
344
+
345
+ This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend with enhanced caching and versioning capabilities.
346
+
106
347
  ## Direct Drizzle Usage with Custom Driver
107
348
 
108
349
  If you prefer to use Drizzle ORM directly without the additional features of Forge-SQL-ORM (like optimistic locking), you can use the custom driver:
@@ -116,24 +357,424 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
116
357
 
117
358
  // Use drizzle directly
118
359
  const users = await db.select().from(users);
360
+ const users = await db.selectAliased(getTableColumns(users)).from(users);
361
+ const users = await db.selectAliasedDistinct(getTableColumns(users)).from(users);
362
+ await db.insert(users)...;
363
+ await db.update(users)...;
364
+ await db.delete(users)...;
365
+ // Use drizzle with kvs cache
366
+ const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
367
+ const users = await db.selectAliasedDistinctCacheable(getTableColumns(users)).from(users);
368
+ await db.insertAndEvictCache(users)...;
369
+ await db.updateAndEvictCache(users)...;
370
+ await db.deleteAndEvictCache(users)...;
371
+
372
+ // Use drizzle with kvs cache context
373
+ await forgeSQL.executeWithCacheContext(async () => {
374
+ await db.insertWithCacheContext(users)...;
375
+ await db.updateWithCacheContext(users)...;
376
+ await db.deleteWithCacheContext(users)...;
377
+ // invoke without cache
378
+ const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
379
+ // Cache is cleared only once at the end for all affected tables
380
+ });
381
+
382
+ // Using new methods with direct drizzle
383
+ const usersFrom = await forgeSQL.selectFrom(users)
384
+ .where(eq(users.active, true));
385
+
386
+ const usersDistinct = await forgeSQL.selectDistinctFrom(users)
387
+ .where(eq(users.active, true));
388
+
389
+ const usersCacheable = await forgeSQL.selectCacheableFrom(users)
390
+ .where(eq(users.active, true));
391
+
392
+ // Raw SQL execution
393
+ const rawUsers = await forgeSQL.execute(
394
+ "SELECT * FROM users WHERE active = ?",
395
+ [true]
396
+ );
397
+
398
+ // Raw SQL with caching
399
+ const cachedRawUsers = await forgeSQL.executeCacheable(
400
+ "SELECT * FROM users WHERE active = ?",
401
+ [true],
402
+ 300
403
+ );
404
+ ```
405
+
406
+ ## Setting Up Caching with @forge/kvs (Optional)
407
+
408
+ The caching system is optional and only needed if you want to use cache-related features. To enable the caching system, you need to install the required dependency and configure your manifest.
409
+
410
+ ### How Caching Works
411
+
412
+ To use caching, you need to use Forge-SQL-ORM methods that support cache management:
413
+
414
+ **Methods that perform cache eviction after execution and in cache context (batch eviction):**
415
+ - `forgeSQL.insertAndEvictCache()`
416
+ - `forgeSQL.updateAndEvictCache()`
417
+ - `forgeSQL.deleteAndEvictCache()`
418
+ - `forgeSQL.modifyWithVersioningAndEvictCache()`
419
+ - `forgeSQL.getDrizzleQueryBuilder().insertAndEvictCache()`
420
+ - `forgeSQL.getDrizzleQueryBuilder().updateAndEvictCache()`
421
+ - `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
422
+
423
+ **Methods that participate in cache context only (batch eviction):**
424
+ - All methods except the default Drizzle methods:
425
+ - `forgeSQL.insert()`
426
+ - `forgeSQL.update()`
427
+ - `forgeSQL.delete()`
428
+ - `forgeSQL.modifyWithVersioning()`
429
+ - `forgeSQL.getDrizzleQueryBuilder().insertWithCacheContext()`
430
+ - `forgeSQL.getDrizzleQueryBuilder().updateWithCacheContext()`
431
+ - `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
432
+
433
+ **Methods do not do evict cache, better do not use with cache feature:**
434
+ - `forgeSQL.getDrizzleQueryBuilder().insert()`
435
+ - `forgeSQL.getDrizzleQueryBuilder().update()`
436
+ - `forgeSQL.getDrizzleQueryBuilder().delete()`
437
+
438
+ **Cacheable methods:**
439
+ - `forgeSQL.selectCacheable()`
440
+ - `forgeSQL.selectDistinctCacheable()`
441
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
442
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
443
+
444
+ **Cache context example:**
445
+ ```typescript
446
+ await forgeSQL.executeWithCacheContext(async () => {
447
+ // These methods participate in batch cache clearing
448
+ await forgeSQL.insert(Users).values(userData);
449
+ await forgeSQL.update(Users).set(updateData).where(eq(Users.id, 1));
450
+ await forgeSQL.delete(Users).where(eq(Users.id, 1));
451
+ // Cache is cleared only once at the end for all affected tables
452
+ });
119
453
  ```
120
454
 
121
- ## Drizzle Usage with forge-sql-orm
122
455
 
123
- If you prefer to use Drizzle ORM with the additional features of Forge-SQL-ORM (like optimistic locking), you can use the custom driver:
456
+ The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
457
+
458
+ 1. Resolver calls forge-sql-orm with a SQL query and parameters.
459
+ 2. forge-sql-orm generates a cache key = hash(sql, parameters).
460
+ 3. It asks @forge/kvs for an existing cached result.
461
+ - Cache hit → result is returned immediately.
462
+ - Cache miss / expired → query is executed against @forge/sql.
463
+ 4. Fresh result is stored in @forge/kvs with TTL and returned to the caller.
464
+
465
+ ![img.png](img/umlCache1.png)
466
+
467
+
468
+ The diagram below shows how Evict Cache works in Forge-SQL-ORM:
469
+
470
+ 1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
471
+ 2. After a successful update, **forge-sql-orm** queries the `cache` entity by using the **`sql` field** with `filter.contains("users")` to find affected cached queries.
472
+ 3. The returned cache entries are deleted in **batches** (up to 25 per transaction).
473
+ 4. Once eviction is complete, the update result is returned to the resolver.
474
+ 5. **Note:** Expired entries are not processed here — they are cleaned up separately by the scheduled cache cleanup trigger using the `expiration` index.
475
+
476
+ ![img.png](img/umlCacheEvict1.png)
124
477
 
478
+ The diagram below shows how Scheduled Expiration Cleanup works:
479
+
480
+ 1. A periodic scheduler (Forge trigger) runs cache cleanup independently of data modifications.
481
+ 2. forge-sql-orm queries the cache entity by the expiration index to find entries with expiration < now.
482
+ 3. Entries are deleted in batches (up to 25 per transaction) until the page is empty; pagination is done with a cursor (e.g., 100 per page).
483
+ 4. This keeps the cache footprint small and prevents stale data accumulation.
484
+
485
+ ![img.png](img/umlCacheEvictScheduler1.png)
486
+
487
+ The diagram below shows how Cache Context works:
488
+
489
+ `executeWithCacheContext(fn)` lets you group multiple data modifications and perform **one consolidated cache eviction** at the end:
490
+
491
+ 1. The context starts with an empty `affectedTables` set.
492
+ 2. Each successful `INSERT/UPDATE/DELETE` inside the context registers its table name in `affectedTables`.
493
+ 3. **Reads inside the same context** that target tables present in `affectedTables` will **bypass the cache** (read-through to SQL) to avoid serving stale data. These reads also **do not write** back to cache until eviction completes.
494
+ 4. On context completion, `affectedTables` is de-duplicated and used to build **one combined KVS query** over the `sql` field with
495
+ `filter.or(filter.contains("<t1>"), filter.contains("<t2>"), ...)`, returning all impacted cache entries in a single scan (paged by cursor, e.g., 100/page).
496
+ 5. Matching cache entries are deleted in **batches** (≤25 per transaction) until the page is exhausted; then the next page is fetched via the cursor.
497
+ 6. Expiration is handled separately by the scheduled cleanup and is **not part of** the context flow.
498
+
499
+ ![img.png](img/umlCacheEvictCacheContext1.png)
500
+
501
+
502
+ ### Important Considerations
503
+
504
+ **@forge/kvs Limits:**
505
+ Please review the [official @forge/kvs quotas and limits](https://developer.atlassian.com/platform/forge/platform-quotas-and-limits/#kvs-and-custom-entity-store-quotas) before implementing caching.
506
+
507
+ **Caching Guidelines:**
508
+ - Don't cache everything - be selective about what to cache
509
+ - Don't cache simple and fast queries - sometimes direct query is faster than cache
510
+ - Consider data size and frequency of changes
511
+ - Monitor cache usage to stay within quotas
512
+ - Use appropriate TTL values
513
+
514
+ ### Step 1: Install Dependencies
515
+
516
+ ```bash
517
+ npm install @forge/kvs -S
518
+ ```
519
+
520
+ ### Step 2: Configure Manifest
521
+
522
+ Add the storage entity configuration and scheduler trigger to your `manifest.yml`:
523
+
524
+ ```yaml
525
+ modules:
526
+ scheduledTrigger:
527
+ - key: clear-cache-trigger
528
+ function: clearCache
529
+ interval: fiveMinute
530
+ storage:
531
+ entities:
532
+ - name: cache
533
+ attributes:
534
+ sql:
535
+ type: string
536
+ expiration:
537
+ type: integer
538
+ data:
539
+ type: string
540
+ indexes:
541
+ - sql
542
+ - expiration
543
+ sql:
544
+ - key: main
545
+ engine: mysql
546
+ function:
547
+ - key: clearCache
548
+ handler: index.clearCache
549
+ ```
550
+ ```typescript
551
+ // Example usage in your Forge app
552
+ import { clearCacheSchedulerTrigger } from "forge-sql-orm";
553
+
554
+ export const clearCache = () => {
555
+ return clearCacheSchedulerTrigger({
556
+ cacheEntityName: "cache",
557
+ });
558
+ };
559
+ ```
560
+
561
+
562
+ ### Step 3: Configure ORM Options
563
+
564
+ Set the cache entity name in your ForgeSQL configuration:
565
+
566
+ ```typescript
567
+ const options = {
568
+ cacheEntityName: "cache", // Must match the entity name in manifest.yml
569
+ cacheTTL: 300, // Default cache TTL in seconds (5 minutes)
570
+ cacheWrapTable: true, // Wrap table names with backticks in cache keys
571
+ // ... other options
572
+ };
573
+
574
+ const forgeSQL = new ForgeSQL(options);
575
+ ```
576
+
577
+ **Important Notes:**
578
+ - The `cacheEntityName` must exactly match the `name` in your manifest storage entities
579
+ - The entity attributes (`sql`, `expiration`, `data`) are required for proper cache functionality
580
+ - Indexes on `sql` and `expiration` improve cache lookup performance
581
+ - Cache data is automatically cleaned up based on TTL settings
582
+ - No additional permissions are required beyond standard Forge app permissions
583
+
584
+ ### Complete Setup Examples
585
+
586
+ **Basic setup (without caching):**
587
+
588
+ **package.json:**
589
+ ```shell
590
+ npm install forge-sql-orm @forge/sql drizzle-orm -S
591
+ ```
592
+
593
+ **manifest.yml:**
594
+ ```yaml
595
+ modules:
596
+ sql:
597
+ - key: main
598
+ engine: mysql
599
+ ```
600
+
601
+ **index.ts:**
125
602
  ```typescript
126
603
  import ForgeSQL from "forge-sql-orm";
604
+
127
605
  const forgeSQL = new ForgeSQL();
128
- forgeSQL.crud().insert(...);
129
- forgeSQL.crud().updateById(...);
130
- const db = forgeSQL.getDrizzleQueryBuilder();
131
606
 
132
- // Use drizzle
133
- const users = await db.select().from(users);
607
+ // simple insert
608
+ await forgeSQL.insert(Users, [userData]);
609
+ // Use versioned operations without caching
610
+ await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
611
+ const users = await forgeSQL.select({id: Users.id});
612
+
613
+
134
614
  ```
135
615
 
136
- This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend.
616
+ **With caching support:**
617
+ ```shell
618
+ npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
619
+ ```
620
+
621
+ **manifest.yml:**
622
+ ```yaml
623
+ modules:
624
+ scheduledTrigger:
625
+ - key: clear-cache-trigger
626
+ function: clearCache
627
+ interval: fiveMinute
628
+ storage:
629
+ entities:
630
+ - name: cache
631
+ attributes:
632
+ sql:
633
+ type: string
634
+ expiration:
635
+ type: integer
636
+ data:
637
+ type: string
638
+ indexes:
639
+ - sql
640
+ - expiration
641
+ sql:
642
+ - key: main
643
+ engine: mysql
644
+ function:
645
+ - key: clearCache
646
+ handler: index.clearCache
647
+ ```
648
+
649
+ **index.ts:**
650
+
651
+ ```typescript
652
+ import ForgeSQL from "forge-sql-orm";
653
+
654
+ const forgeSQL = new ForgeSQL({
655
+ cacheEntityName: "cache"
656
+ });
657
+
658
+ import {clearCacheSchedulerTrigger} from "forge-sql-orm";
659
+ import {getTableColumns} from "drizzle-orm";
660
+
661
+ export const clearCache = () => {
662
+ return clearCacheSchedulerTrigger({
663
+ cacheEntityName: "cache",
664
+ });
665
+ };
666
+
667
+
668
+ // Now you can use caching features
669
+ const usersData = await forgeSQL.selectCacheable(getTableColumns(users)).from(users).where(eq(users.active, true))
670
+
671
+ // simple insert
672
+ await forgeSQL.insertAndEvictCache(users, [userData]);
673
+ // Use versioned operations with caching
674
+ await forgeSQL.modifyWithVersioningAndEvictCache().insert(users, [userData]);
675
+
676
+ // use Cache Context
677
+ const data = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
678
+ // after insert mark users to evict
679
+ await forgeSQL.insert(users, [userData]);
680
+ // after insertAndEvictCache mark orders to evict
681
+ await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
682
+ // execute query and put result to local cache
683
+ await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
684
+ .from(users)
685
+ .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
686
+ // use local cache without @forge/kvs and @forge/sql
687
+ return await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
688
+ .from(users)
689
+ .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
690
+ })
691
+ // execute query and put result to kvs cache
692
+ await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
693
+ .from(users)
694
+ .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
695
+
696
+ // get result from @foge/kvs cache without real @forge/sql call
697
+ await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
698
+ .from(users)
699
+ .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
700
+
701
+ // use Local Cache for performance optimization
702
+ const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
703
+ // First query - hits database and caches result
704
+ const users = await forgeSQL.select({id: users.id, name: users.name})
705
+ .from(users).where(eq(users.active, true));
706
+
707
+ // Second query - uses local cache (no database call)
708
+ const cachedUsers = await forgeSQL.select({id: users.id, name: users.name})
709
+ .from(users).where(eq(users.active, true));
710
+
711
+ // Using new methods for better performance
712
+ const usersFrom = await forgeSQL.selectFrom(users)
713
+ .where(eq(users.active, true));
714
+
715
+ // This will use local cache (no database call)
716
+ const cachedUsersFrom = await forgeSQL.selectFrom(users)
717
+ .where(eq(users.active, true));
718
+
719
+ // Raw SQL with local caching
720
+ const rawUsers = await forgeSQL.execute(
721
+ "SELECT id, name FROM users WHERE active = ?",
722
+ [true]
723
+ );
724
+
725
+ // Insert operation - evicts local cache
726
+ await forgeSQL.insert(users).values({name: 'New User', active: true});
727
+
728
+ // Third query - hits database again and caches new result
729
+ const updatedUsers = await forgeSQL.select({id: users.id, name: users.name})
730
+ .from(users).where(eq(users.active, true));
731
+
732
+ return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
733
+ });
734
+
735
+ ```
736
+
737
+ ## Choosing the Right Method - ForgeSQL ORM
738
+
739
+ ### When to Use Each Approach
740
+
741
+ | Method | Use Case | Versioning | Cache Management |
742
+ |--------|----------|------------|------------------|
743
+ | `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support| ✅ Yes | ✅ Yes |
744
+ | `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
745
+ | `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
746
+ | `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
747
+ | `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
748
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
749
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
750
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
751
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
752
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
753
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
754
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
755
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
756
+
757
+
758
+ ## Choosing the Right Method - Direct Drizzle
759
+
760
+ ### When to Use Each Approach
761
+
762
+ | Method | Use Case | Versioning | Cache Management |
763
+ |--------|----------|------------|------------------|
764
+ | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
765
+ | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
766
+ | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
767
+ | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
768
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
769
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
770
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
771
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
772
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
773
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
774
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
775
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
776
+ where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
777
+
137
778
 
138
779
  ## Step-by-Step Migration Workflow
139
780
 
@@ -344,6 +985,49 @@ const users = await db.select().from(users);
344
985
  ### Basic Fetch Operations
345
986
 
346
987
  ```js
988
+ // Using forgeSQL.select()
989
+ const user = await forgeSQL
990
+ .select({user: users})
991
+ .from(users);
992
+
993
+ // Using forgeSQL.selectDistinct()
994
+ const user = await forgeSQL
995
+ .selectDistinct({user: users})
996
+ .from(users);
997
+
998
+ // Using forgeSQL.selectCacheable()
999
+ const user = await forgeSQL
1000
+ .selectCacheable({user: users})
1001
+ .from(users);
1002
+
1003
+ // Using forgeSQL.selectFrom() - Select all columns with field aliasing
1004
+ const user = await forgeSQL
1005
+ .selectFrom(users)
1006
+ .where(eq(users.id, 1));
1007
+
1008
+ // Using forgeSQL.selectDistinctFrom() - Select distinct all columns with field aliasing
1009
+ const user = await forgeSQL
1010
+ .selectDistinctFrom(users)
1011
+ .where(eq(users.id, 1));
1012
+
1013
+ // Using forgeSQL.selectCacheableFrom() - Select all columns with field aliasing and caching
1014
+ const user = await forgeSQL
1015
+ .selectCacheableFrom(users)
1016
+ .where(eq(users.id, 1));
1017
+
1018
+ // Using forgeSQL.selectDistinctCacheableFrom() - Select distinct all columns with field aliasing and caching
1019
+ const user = await forgeSQL
1020
+ .selectDistinctCacheableFrom(users)
1021
+ .where(eq(users.id, 1));
1022
+
1023
+ // Using forgeSQL.execute() - Execute raw SQL with local caching
1024
+ const user = await forgeSQL
1025
+ .execute("SELECT * FROM users WHERE id = ?", [1]);
1026
+
1027
+ // Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
1028
+ const user = await forgeSQL
1029
+ .executeCacheable("SELECT * FROM users WHERE id = ?", [1], 300);
1030
+
347
1031
  // Using forgeSQL.getDrizzleQueryBuilder()
348
1032
  const user = await forgeSQL
349
1033
  .getDrizzleQueryBuilder()
@@ -399,6 +1083,31 @@ const orderWithUser = await forgeSQL
399
1083
  .from(orders)
400
1084
  .innerJoin(users, eq(orders.userId, users.id));
401
1085
 
1086
+ // Using new selectFrom methods with joins
1087
+ const orderWithUser = await forgeSQL
1088
+ .selectFrom(orders)
1089
+ .innerJoin(users, eq(orders.userId, users.id))
1090
+ .where(eq(orders.id, 1));
1091
+
1092
+ // Using selectCacheableFrom with joins and caching
1093
+ const orderWithUser = await forgeSQL
1094
+ .selectCacheableFrom(orders)
1095
+ .innerJoin(users, eq(orders.userId, users.id))
1096
+ .where(eq(orders.id, 1));
1097
+
1098
+ // Using with() for Common Table Expressions (CTEs)
1099
+ const userStats = await forgeSQL
1100
+ .with(
1101
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1102
+ forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1103
+ )
1104
+ .select({
1105
+ totalActiveUsers: sql`COUNT(au.id)`,
1106
+ totalCompletedOrders: sql`COUNT(co.id)`
1107
+ })
1108
+ .from(sql`activeUsers au`)
1109
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
1110
+
402
1111
  // OR with direct drizzle
403
1112
  const db = patchDbWithSelectAliased(drizzle(forgeDriver));
404
1113
  const orderWithUser = await db
@@ -440,32 +1149,125 @@ const userStats = await forgeSQL
440
1149
  const users = await forgeSQL
441
1150
  .fetch()
442
1151
  .executeRawSQL<Users>("SELECT * FROM users");
1152
+
1153
+ // Using execute() for raw SQL with local caching
1154
+ const users = await forgeSQL
1155
+ .execute("SELECT * FROM users WHERE active = ?", [true]);
1156
+
1157
+ // Using executeCacheable() for raw SQL with local and global caching
1158
+ const users = await forgeSQL
1159
+ .executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
1160
+
1161
+ // Using execute() with complex queries
1162
+ const userStats = await forgeSQL
1163
+ .execute(`
1164
+ SELECT
1165
+ u.id,
1166
+ u.name,
1167
+ COUNT(o.id) as order_count,
1168
+ SUM(o.amount) as total_amount
1169
+ FROM users u
1170
+ LEFT JOIN orders o ON u.id = o.user_id
1171
+ WHERE u.active = ?
1172
+ GROUP BY u.id, u.name
1173
+ `, [true]);
443
1174
  ```
444
1175
 
445
- ## CRUD Operations
1176
+ ## Modify Operations
446
1177
 
447
- ### Insert Operations
1178
+ Forge-SQL-ORM provides multiple approaches for Modify operations, each with different characteristics:
448
1179
 
449
- ```js
450
- // Single insert
451
- const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
1180
+ ### 1. Basic Drizzle Operations (Cache Context Aware)
452
1181
 
453
- // Bulk insert
454
- await forgeSQL.crud().insert(Users, [
1182
+ These operations work like standard Drizzle methods but participate in cache context when used within `executeWithCacheContext()`:
1183
+
1184
+ ```js
1185
+ // Basic insert (participates in cache context when used within executeWithCacheContext)
1186
+ await forgeSQL.insert(Users).values({ id: 1, name: "Smith" });
1187
+
1188
+ // Basic update (participates in cache context when used within executeWithCacheContext)
1189
+ await forgeSQL.update(Users)
1190
+ .set({ name: "Smith Updated" })
1191
+ .where(eq(Users.id, 1));
1192
+
1193
+ // Basic delete (participates in cache context when used within executeWithCacheContext)
1194
+ await forgeSQL.delete(Users)
1195
+ .where(eq(Users.id, 1));
1196
+ ```
1197
+
1198
+ ### 2. Non-Versioned Operations with Cache Management
1199
+
1200
+ These operations don't use optimistic locking but provide cache invalidation:
1201
+
1202
+ ```js
1203
+ // Insert without versioning but with cache invalidation
1204
+ await forgeSQL.insertAndEvictCache(Users).values({ id: 1, name: "Smith" });
1205
+
1206
+ // Update without versioning but with cache invalidation
1207
+ await forgeSQL.updateAndEvictCache(Users)
1208
+ .set({ name: "Smith Updated" })
1209
+ .where(eq(Users.id, 1));
1210
+
1211
+ // Delete without versioning but with cache invalidation
1212
+ await forgeSQL.deleteAndEvictCache(Users)
1213
+ .where(eq(Users.id, 1));
1214
+ ```
1215
+
1216
+ ### 3. Versioned Operations with Cache Management (Recommended)
1217
+
1218
+ These operations use optimistic locking and automatic cache invalidation:
1219
+
1220
+ ```js
1221
+ // Insert with versioning and cache management
1222
+ const userId = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [{ id: 1, name: "Smith" }]);
1223
+
1224
+ // Bulk insert with versioning
1225
+ await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [
455
1226
  { id: 2, name: "Smith" },
456
1227
  { id: 3, name: "Vasyl" },
457
1228
  ]);
458
1229
 
459
- // Insert with duplicate handling
460
- await forgeSQL.crud().insert(
461
- Users,
462
- [
463
- { id: 4, name: "Smith" },
464
- { id: 4, name: "Vasyl" },
465
- ],
466
- true
467
- );
1230
+ // Update by ID with optimistic locking and cache invalidation
1231
+ await forgeSQL.modifyWithVersioningAndEvictCache().updateById({ id: 1, name: "Smith Updated" }, Users);
468
1232
 
1233
+ // Delete by ID with versioning and cache invalidation
1234
+ await forgeSQL.modifyWithVersioningAndEvictCache().deleteById(1, Users);
1235
+ ```
1236
+
1237
+ ### 4. Versioned Operations without Cache Management
1238
+
1239
+ These operations use optimistic locking but don't manage cache:
1240
+
1241
+ ```js
1242
+ // Insert with versioning only (no cache management)
1243
+ const userId = await forgeSQL.modifyWithVersioning().insert(Users, [{ id: 1, name: "Smith" }]);
1244
+
1245
+ // Update with versioning only
1246
+ await forgeSQL.modifyWithVersioning().updateById({ id: 1, name: "Smith Updated" }, Users);
1247
+
1248
+ // Delete with versioning only
1249
+ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
1250
+ ```
1251
+
1252
+ ### 5. Legacy Modify Operations (Removed in 2.1.x)
1253
+
1254
+ ⚠️ **BREAKING CHANGE**: The `crud()` and `modify()` methods have been completely removed in version 2.1.x.
1255
+
1256
+ ```js
1257
+ // ❌ These methods no longer exist in 2.1.x
1258
+ // const userId = await forgeSQL.crud().insert(Users, [{ id: 1, name: "Smith" }]);
1259
+ // await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
1260
+ // await forgeSQL.crud().deleteById(1, Users);
1261
+
1262
+ // ✅ Use the new methods instead
1263
+ const userId = await forgeSQL.modifyWithVersioning().insert(Users, [{ id: 1, name: "Smith" }]);
1264
+ await forgeSQL.modifyWithVersioning().updateById({ id: 1, name: "Smith Updated" }, Users);
1265
+ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
1266
+ ```
1267
+
1268
+ ### Advanced Operations
1269
+
1270
+ ```js
469
1271
  // Insert with sequence (nextVal)
470
1272
  import { nextVal } from "forge-sql-orm";
471
1273
 
@@ -474,38 +1276,24 @@ const user = {
474
1276
  name: "user test",
475
1277
  organization_id: 1
476
1278
  };
477
- const id = await forgeSQL.modify().insert(appUser, [user]);
478
-
479
- // The generated SQL will be:
480
- // INSERT INTO app_user (id, name, organization_id)
481
- // VALUES (NEXTVAL(user_id_seq), ?, ?) -- params: ["user test", 1]
482
- ```
483
-
484
- ### Update Operations
485
-
486
- ```js
487
- // Update by ID with optimistic locking
488
- await forgeSQL.crud().updateById({ id: 1, name: "Smith Updated" }, Users);
489
-
490
- // Update specific fields
491
- await forgeSQL.crud().updateById(
492
- { id: 1, age: 35 },
493
- Users
494
- );
1279
+ const id = await forgeSQL.modifyWithVersioning().insert(appUser, [user]);
495
1280
 
496
1281
  // Update with custom WHERE condition
497
- await forgeSQL.crud().updateFields(
1282
+ await forgeSQL.modifyWithVersioning().updateFields(
498
1283
  { name: "New Name", age: 35 },
499
1284
  Users,
500
1285
  eq(Users.email, "smith@example.com")
501
1286
  );
502
- ```
503
-
504
- ### Delete Operations
505
1287
 
506
- ```js
507
- // Delete by ID
508
- await forgeSQL.crud().deleteById(1, Users);
1288
+ // Insert with duplicate handling
1289
+ await forgeSQL.modifyWithVersioning().insert(
1290
+ Users,
1291
+ [
1292
+ { id: 4, name: "Smith" },
1293
+ { id: 4, name: "Vasyl" },
1294
+ ],
1295
+ true
1296
+ );
509
1297
  ```
510
1298
 
511
1299
  ## SQL Utilities
@@ -543,8 +1331,332 @@ const result = await forgeSQL
543
1331
  - This prevents SQL injection by ensuring only numeric values are inserted
544
1332
  - Always use this function instead of string concatenation for LIMIT and OFFSET values
545
1333
 
1334
+ ## Global Cache System (Level 2)
1335
+
1336
+ [↑ Back to Top](#table-of-contents)
1337
+
1338
+ Forge-SQL-ORM includes a sophisticated global caching system that provides **cross-invocation caching** - the ability to share cached data between different resolver invocations. The global cache system is built on top of [@forge/kvs Custom entity store](https://developer.atlassian.com/platform/forge/storage-reference/storage-api-custom-entities/) and provides persistent cross-invocation caching with automatic serialization/deserialization of complex data structures.
1339
+
1340
+ ### Cache Levels Overview
1341
+
1342
+ Forge-SQL-ORM implements a two-level caching architecture:
1343
+
1344
+ - **Level 1 (Local Cache)**: In-memory caching within a single resolver invocation scope
1345
+ - **Level 2 (Global Cache)**: Cross-invocation persistent caching using KVS storage
1346
+
1347
+ This multi-level approach provides optimal performance by checking the fastest cache first, then falling back to cross-invocation persistent storage.
1348
+
1349
+ ### Cache Configuration
1350
+
1351
+ The caching system uses Atlassian Forge's Custom entity store to persist cache data. Each cache entry is stored as a custom entity with automatic TTL management and efficient key-based retrieval.
1352
+
1353
+ ```typescript
1354
+ const options = {
1355
+ cacheEntityName: "cache", // KVS Custom entity name for cache storage
1356
+ cacheTTL: 300, // Default cache TTL in seconds (5 minutes)
1357
+ cacheWrapTable: true, // Wrap table names with backticks in cache keys
1358
+ additionalMetadata: {
1359
+ users: {
1360
+ tableName: "users",
1361
+ versionField: {
1362
+ fieldName: "updatedAt",
1363
+ }
1364
+ }
1365
+ }
1366
+ };
1367
+
1368
+ const forgeSQL = new ForgeSQL(options);
1369
+ ```
1370
+
1371
+ ### How Caching Works with @forge/kvs
1372
+
1373
+ The caching system leverages Forge's Custom entity store to provide:
1374
+
1375
+ - **Persistent Storage**: Cache data survives app restarts and deployments
1376
+ - **Automatic TTL**: Built-in expiration handling through Forge's entity lifecycle
1377
+ - **Efficient Retrieval**: Fast key-based lookups using Forge's optimized storage
1378
+ - **Data Serialization**: Automatic handling of complex objects and query results
1379
+ - **Batch Operations**: Efficient bulk cache operations for better performance
1380
+
1381
+ ```typescript
1382
+ // Cache entries are stored as custom entities in Forge's KVS
1383
+ // Example cache key structure:
1384
+ // Key: "CachedQuery_8d74bdd9d85064b72fb2ee072ca948e5"
1385
+ // Value: { data: [...], expiration: 1234567890, sql: "select * from 1" }
1386
+ ```
1387
+
1388
+
1389
+ ### Cache Context Operations
1390
+
1391
+ The cache context allows you to batch cache invalidation events and bypass cache reads for affected tables:
1392
+
1393
+ ```typescript
1394
+ // Execute operations within a cache context
1395
+ await forgeSQL.executeWithCacheContext(async () => {
1396
+ // All cache invalidation events are collected and executed in batch
1397
+ await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
1398
+ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(updateData, Users);
1399
+ // Cache is cleared only once at the end for all affected tables
1400
+ });
1401
+
1402
+ // Execute with return value
1403
+ const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
1404
+ const user = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
1405
+ return user;
1406
+ });
1407
+
1408
+ // Basic operations also participate in cache context
1409
+ await forgeSQL.executeWithCacheContext(async () => {
1410
+ // These operations will participate in batch cache clearing
1411
+ await forgeSQL.insert(Users).values(userData);
1412
+ await forgeSQL.update(Users).set(updateData).where(eq(Users.id, 1));
1413
+ await forgeSQL.delete(Users).where(eq(Users.id, 1));
1414
+ // Cache is cleared only once at the end for all affected tables
1415
+ });
1416
+ ```
1417
+
1418
+ ### Local Cache Operations (Level 1)
1419
+
1420
+ Forge-SQL-ORM provides a local cache system (Level 1 cache) that stores query results in memory for the duration of a single resolver invocation. This is particularly useful for optimizing repeated queries within the same execution context(resolver invocation).
1421
+
1422
+ #### What is Local Cache?
1423
+
1424
+ Local cache is an in-memory caching layer that operates within a single resolver invocation scope. Unlike the global KVS cache, local cache:
1425
+
1426
+ - **Stores data in memory** using Node.js `AsyncLocalStorage`
1427
+ - **Automatically clears** when the invocation completes (Resolver call)
1428
+ - **Provides instant access** to previously executed queries in resolver invocation
1429
+ - **Reduces database load** for repeated operations within the same invocation
1430
+ - **Works alongside** the global KVS cache system
1431
+
1432
+ #### Key Features of Local Cache
1433
+
1434
+ - **In-Memory Storage**: Query results are cached in memory using Node.js `AsyncLocalStorage`
1435
+ - **Invocation-Scoped**: Cache is automatically cleared when the invocation completes
1436
+ - **Automatic Eviction**: Cache is cleared when insert/update/delete operations are performed
1437
+ - **No Persistence**: Data is not stored between Invocations (unlike global KVS cache)
1438
+ - **Performance Optimization**: Reduces database queries for repeated operations
1439
+ - **Simple Configuration**: Works out of the box with simple setup
1440
+
1441
+ #### Usage Examples
1442
+
1443
+ ##### Basic Local Cache Usage
1444
+
1445
+ ```typescript
1446
+ // Execute operations within a local cache context
1447
+ await forgeSQL.executeWithLocalContext(async () => {
1448
+ // First call - executes query and caches result
1449
+ const users = await forgeSQL.select({ id: users.id, name: users.name })
1450
+ .from(users).where(eq(users.active, true));
1451
+
1452
+ // Second call - gets result from local cache (no database query)
1453
+ const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1454
+ .from(users).where(eq(users.active, true));
1455
+
1456
+ // Using new selectFrom methods with local caching
1457
+ const usersFrom = await forgeSQL.selectFrom(users)
1458
+ .where(eq(users.active, true));
1459
+
1460
+ // This will use local cache (no database call)
1461
+ const cachedUsersFrom = await forgeSQL.selectFrom(users)
1462
+ .where(eq(users.active, true));
1463
+
1464
+ // Using execute() with local caching
1465
+ const rawUsers = await forgeSQL.execute(
1466
+ "SELECT id, name FROM users WHERE active = ?",
1467
+ [true]
1468
+ );
1469
+
1470
+ // This will use local cache (no database call)
1471
+ const cachedRawUsers = await forgeSQL.execute(
1472
+ "SELECT id, name FROM users WHERE active = ?",
1473
+ [true]
1474
+ );
1475
+
1476
+ // Insert operation - evicts local cache for users table
1477
+ await forgeSQL.insert(users).values({ name: 'New User', active: true });
1478
+
1479
+ // Third call - executes query again and caches new result
1480
+ const updatedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1481
+ .from(users).where(eq(users.active, true));
1482
+ });
1483
+
1484
+ // Execute with return value
1485
+ const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1486
+ // First call - executes query and caches result
1487
+ const users = await forgeSQL.select({ id: users.id, name: users.name })
1488
+ .from(users).where(eq(users.active, true));
1489
+
1490
+ // Second call - gets result from local cache (no database query)
1491
+ const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1492
+ .from(users).where(eq(users.active, true));
1493
+
1494
+ return { users, cachedUsers };
1495
+ });
1496
+ ```
1497
+
1498
+ ##### Real-World Resolver Example
1499
+
1500
+ ```typescript
1501
+ // Atlassian forge resolver with local cache optimization
1502
+ const userResolver = async (req) => {
1503
+ return await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1504
+ // Get user details using selectFrom (all columns with field aliasing)
1505
+ const user = await forgeSQL.selectFrom(users)
1506
+ .where(eq(users.id, args.userId));
1507
+
1508
+ // Get user's orders using selectCacheableFrom (with caching)
1509
+ const orders = await forgeSQL.selectCacheableFrom(orders)
1510
+ .where(eq(orders.userId, args.userId));
1511
+
1512
+ // Get user's profile using raw SQL with execute()
1513
+ const profile = await forgeSQL.execute(
1514
+ "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1515
+ [args.userId]
1516
+ );
1517
+
1518
+ // Get user statistics using complex raw SQL
1519
+ const stats = await forgeSQL.execute(`
1520
+ SELECT
1521
+ COUNT(o.id) as total_orders,
1522
+ SUM(o.amount) as total_spent,
1523
+ AVG(o.amount) as avg_order_value
1524
+ FROM orders o
1525
+ WHERE o.user_id = ? AND o.status = 'completed'
1526
+ `, [args.userId]);
1527
+
1528
+ // If any of these queries are repeated within the same resolver,
1529
+ // they will use the local cache instead of hitting the database
1530
+
1531
+ return {
1532
+ ...user[0],
1533
+ orders,
1534
+ profile: profile[0],
1535
+ stats: stats[0]
1536
+ };
1537
+ });
1538
+ };
1539
+ ```
1540
+
1541
+
1542
+ #### Local Cache (Level 1) vs Global Cache (Level 2)
1543
+
1544
+ | Feature | Local Cache (Level 1) | Global Cache (Level 2) |
1545
+ |---------|----------------------|------------------------|
1546
+ | **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
1547
+ | **Scope** | Single forge invocation | Cross-invocation (between calls) |
1548
+ | **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
1549
+ | **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
1550
+ | **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
1551
+ | **Use Case** | Invocation optimization | Cross-invocation data sharing |
1552
+ | **Configuration** | None required | Requires KVS setup |
1553
+ | **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
1554
+ | **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
1555
+ | **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
1556
+
1557
+ #### Integration with Global Cache (Level 2)
1558
+
1559
+ Local cache (Level 1) works alongside the global cache (Level 2) system:
1560
+
1561
+ ```typescript
1562
+ // Multi-level cache checking: Level 1 → Level 2 → Database
1563
+ await forgeSQL.executeWithLocalContext(async () => {
1564
+ // This will check:
1565
+ // 1. Local cache (Level 1 - in-memory)
1566
+ // 2. Global cache (Level 2 - KVS)
1567
+ // 3. Database query
1568
+ const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
1569
+ .from(users).where(eq(users.active, true));
1570
+
1571
+ // Using new methods with multi-level caching
1572
+ const usersFrom = await forgeSQL.selectCacheableFrom(users)
1573
+ .where(eq(users.active, true));
1574
+
1575
+ // Raw SQL with multi-level caching
1576
+ const rawUsers = await forgeSQL.executeCacheable(
1577
+ "SELECT id, name FROM users WHERE active = ?",
1578
+ [true],
1579
+ 300 // TTL in seconds
1580
+ );
1581
+ });
1582
+ ```
1583
+
1584
+ #### Local Cache Flow Diagram
1585
+
1586
+ The diagram below shows how local cache works in Forge-SQL-ORM:
1587
+
1588
+ 1. **Request Start**: Local cache context is initialized with empty cache
1589
+ 2. **First Query**: Cache miss → Global cache miss → Database query → Save to local cache
1590
+ 3. **Repeated Query**: Cache hit → Return cached result (no database call)
1591
+ 4. **Data Modification**: Insert/Update/Delete → Evict local cache for affected table
1592
+ 5. **Query After Modification**: Cache miss (was evicted) → Database query → Save to local cache
1593
+ 6. **Request End**: Local cache context is destroyed, all data cleared
1594
+
1595
+ ![Local Cache Flow](img/localCacheFlow.txt)
1596
+
1597
+ ### Cache-Aware Query Operations
1598
+
1599
+ ```typescript
1600
+ // Execute queries with caching
1601
+ const users = await forgeSQL.modifyWithVersioningAndEvictCache().executeQuery(
1602
+ forgeSQL.select().from(Users).where(eq(Users.active, true)),
1603
+ 600 // Custom TTL in seconds
1604
+ );
1605
+
1606
+ // Execute single result queries with caching
1607
+ const user = await forgeSQL.modifyWithVersioningAndEvictCache().executeQueryOnlyOne(
1608
+ forgeSQL.select().from(Users).where(eq(Users.id, 1))
1609
+ );
1610
+
1611
+ // Execute raw SQL with caching
1612
+ const results = await forgeSQL.modifyWithVersioningAndEvictCache().executeRawSQL(
1613
+ "SELECT * FROM users WHERE active = ?",
1614
+ [true],
1615
+ 300 // TTL in seconds
1616
+ );
1617
+
1618
+ // Using new methods for cache-aware operations
1619
+ const usersFrom = await forgeSQL.selectCacheableFrom(Users)
1620
+ .where(eq(Users.active, true));
1621
+
1622
+ const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1623
+ .where(eq(Users.active, true));
1624
+
1625
+ // Raw SQL with local and global caching
1626
+ const rawUsers = await forgeSQL.executeCacheable(
1627
+ "SELECT * FROM users WHERE active = ?",
1628
+ [true],
1629
+ 300 // TTL in seconds
1630
+ );
1631
+
1632
+ // Using with() for Common Table Expressions with caching
1633
+ const userStats = await forgeSQL
1634
+ .with(
1635
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1636
+ forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1637
+ )
1638
+ .select({
1639
+ totalActiveUsers: sql`COUNT(au.id)`,
1640
+ totalCompletedOrders: sql`COUNT(co.id)`
1641
+ })
1642
+ .from(sql`activeUsers au`)
1643
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
1644
+ ```
1645
+
1646
+ ### Manual Cache Management
1647
+
1648
+ ```typescript
1649
+ // Clear cache for specific tables
1650
+ await forgeSQL.modifyWithVersioningAndEvictCache().evictCache(["users", "orders"]);
1651
+
1652
+ // Clear cache for specific entities
1653
+ await forgeSQL.modifyWithVersioningAndEvictCache().evictCacheEntities([Users, Orders]);
1654
+ ```
1655
+
546
1656
  ## Optimistic Locking
547
1657
 
1658
+ [↑ Back to Top](#table-of-contents)
1659
+
548
1660
  Optimistic locking is a concurrency control mechanism that prevents data conflicts when multiple transactions attempt to update the same record concurrently. Instead of using locks, this technique relies on a version field in your entity models.
549
1661
 
550
1662
  ### Supported Version Field Types
@@ -575,7 +1687,19 @@ const forgeSQL = new ForgeSQL(options);
575
1687
 
576
1688
  ```typescript
577
1689
  // The version field will be automatically handled
578
- await forgeSQL.crud().updateById(
1690
+ await forgeSQL.modifyWithVersioning().updateById(
1691
+ {
1692
+ id: 1,
1693
+ name: "Updated Name",
1694
+ updatedAt: new Date() // Will be automatically set if not provided
1695
+ },
1696
+ Users
1697
+ );
1698
+ ```
1699
+ or with cache support
1700
+ ```typescript
1701
+ // The version field will be automatically handled
1702
+ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
579
1703
  {
580
1704
  id: 1,
581
1705
  name: "Updated Name",
@@ -594,10 +1718,46 @@ The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
594
1718
  | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
595
1719
  | `disableOptimisticLocking` | `boolean` | Disables optimistic locking. When set to `true`, no additional condition (e.g., a version check) is added during record updates, which can improve performance. However, this may lead to conflicts when multiple transactions attempt to update the same record concurrently. |
596
1720
  | `additionalMetadata` | `object` | Allows adding custom metadata to all entities. This is useful for tracking common fields across all tables (e.g., `createdAt`, `updatedAt`, `createdBy`, etc.). The metadata will be automatically added to all generated entities. |
1721
+ | `cacheEntityName` | `string` | KVS Custom entity name for cache storage. Must match the `name` in your `manifest.yml` storage entities configuration. Required for caching functionality. Defaults to `"cache"`. |
1722
+ | `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
1723
+ | `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
1724
+ | `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
597
1725
 
598
1726
  ## CLI Commands
599
1727
 
600
- Documentation [here](forge-sql-orm-cli/README.md)
1728
+ Forge-SQL-ORM provides a command-line interface for managing database migrations and model generation.
1729
+
1730
+ **📖 [Full CLI Documentation](forge-sql-orm-cli/README.md)** - Complete CLI reference with all commands and options.
1731
+
1732
+ ### Quick CLI Reference
1733
+
1734
+ The CLI tool provides the following main commands:
1735
+
1736
+ - `generate:model` - Generate Drizzle ORM models from your database schema
1737
+ - `migrations:create` - Create new migration files
1738
+ - `migrations:update` - Update existing migrations with schema changes
1739
+ - `migrations:drop` - Create migration to drop tables
1740
+
1741
+ ### Installation
1742
+
1743
+ ```bash
1744
+ npm install -g forge-sql-orm-cli
1745
+ ```
1746
+
1747
+ ### Basic Usage
1748
+
1749
+ ```bash
1750
+ # Generate models from database
1751
+ forge-sql-orm-cli generate:model --dbName myapp --output ./database/entities
1752
+
1753
+ # Create migration
1754
+ forge-sql-orm-cli migrations:create --dbName myapp --entitiesPath ./database/entities
1755
+
1756
+ # Update migration
1757
+ forge-sql-orm-cli migrations:update --dbName myapp --entitiesPath ./database/entities
1758
+ ```
1759
+
1760
+ For detailed information about all available options and advanced usage, see the [Full CLI Documentation](forge-sql-orm-cli/README.md).
601
1761
 
602
1762
  ## Web Triggers for Migrations
603
1763
 
@@ -718,6 +1878,41 @@ CREATE TABLE IF NOT EXISTS orders (...);
718
1878
  SET foreign_key_checks = 1;
719
1879
  ```
720
1880
 
1881
+ ### 4. Clear Cache Scheduler Trigger
1882
+
1883
+ This trigger automatically cleans up expired cache entries based on their TTL (Time To Live). It's useful for:
1884
+ - Automatic cache maintenance
1885
+ - Preventing cache storage from growing indefinitely
1886
+ - Ensuring optimal cache performance
1887
+ - Reducing storage costs
1888
+
1889
+ ```typescript
1890
+ // Example usage in your Forge app
1891
+ import { clearCacheSchedulerTrigger } from "forge-sql-orm";
1892
+
1893
+ export const clearCache = () => {
1894
+ return clearCacheSchedulerTrigger({
1895
+ cacheEntityName: "cache",
1896
+ });
1897
+ };
1898
+ ```
1899
+
1900
+ Configure in `manifest.yml`:
1901
+ ```yaml
1902
+ scheduledTrigger:
1903
+ - key: clear-cache-trigger
1904
+ function: clearCache
1905
+ interval: fiveMinute
1906
+ function:
1907
+ - key: clearCache
1908
+ handler: index.clearCache
1909
+ ```
1910
+
1911
+ **Available Intervals**:
1912
+ - `fiveMinute` - Every 5 minutes
1913
+ - `hour` - Every hour
1914
+ - `day` - Every day
1915
+
721
1916
  ### Important Notes
722
1917
 
723
1918
  **Security Considerations**:
@@ -733,34 +1928,20 @@ SET foreign_key_checks = 1;
733
1928
 
734
1929
  ## Query Analysis and Performance Optimization
735
1930
 
736
- ⚠️ **IMPORTANT NOTE**: The query analysis features described below are experimental and should be used only for troubleshooting purposes. These features rely on TiDB's `information_schema` and `performance_schema` which may change in future updates. As of April 2025, these features are available but their future availability is not guaranteed.
1931
+ [↑ Back to Top](#table-of-contents)
1932
+
1933
+ Forge-SQL-ORM provides comprehensive query analysis tools to help you optimize your database queries and identify performance bottlenecks.
737
1934
 
738
1935
  ### About Atlassian's Built-in Analysis Tools
739
1936
 
740
- Atlassian already provides comprehensive query analysis tools in the development console, including:
1937
+ Atlassian provides comprehensive query analysis tools in the development console, including:
741
1938
  - Basic query performance metrics
742
1939
  - Slow query tracking (queries over 500ms)
743
1940
  - Basic execution statistics
744
1941
  - Query history and patterns
745
1942
 
746
- Our analysis tools are designed to complement these built-in features by providing additional insights directly from TiDB's system schemas. However, they should be used with caution and only for troubleshooting purposes.
747
-
748
- ### Usage Guidelines
1943
+ Our analysis tools complement these built-in features by providing additional insights directly from TiDB's system schemas.
749
1944
 
750
- 1. **Development and Troubleshooting Only**
751
- - These tools should not be used in production code
752
- - Intended only for development and debugging
753
- - Use for identifying and fixing performance issues
754
-
755
- 2. **Schema Stability**
756
- - Features rely on TiDB's `information_schema` and `performance_schema`
757
- - Schema structure may change in future TiDB updates
758
- - No guarantee of long-term availability
759
-
760
- 3. **Current Availability (April 2025)**
761
- - `information_schema` based analysis is currently functional
762
- - Query plan analysis is available
763
- - Performance metrics collection is working
764
1945
 
765
1946
  ### Available Analysis Tools
766
1947
 
@@ -773,10 +1954,10 @@ const analyzeForgeSql = forgeSQL.analyze();
773
1954
 
774
1955
  #### Query Plan Analysis
775
1956
 
776
- ⚠️ **For Troubleshooting Only**: This feature should only be used during development and debugging sessions.
1957
+ Query plan analysis helps you understand how your queries are executed and identify optimization opportunities.
777
1958
 
778
1959
  ```typescript
779
- // Example usage for troubleshooting a specific query
1960
+ // Example usage for analyzing a specific query
780
1961
  const forgeSQL = new ForgeSQL();
781
1962
  const analyzeForgeSql = forgeSQL.analyze();
782
1963
 
@@ -801,15 +1982,163 @@ const rawPlan = await analyzeForgeSql.explainRaw(
801
1982
  "SELECT * FROM users WHERE id = ?",
802
1983
  [1]
803
1984
  );
1985
+
1986
+ // Analyze new methods
1987
+ const usersFromPlan = await analyzeForgeSql.explain(
1988
+ forgeSQL.selectFrom(users).where(eq(users.active, true))
1989
+ );
1990
+
1991
+ const usersCacheablePlan = await analyzeForgeSql.explain(
1992
+ forgeSQL.selectCacheableFrom(users).where(eq(users.active, true))
1993
+ );
1994
+
1995
+ // Analyze Common Table Expressions (CTEs)
1996
+ const ctePlan = await analyzeForgeSql.explain(
1997
+ forgeSQL
1998
+ .with(
1999
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2000
+ forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2001
+ )
2002
+ .select({
2003
+ totalActiveUsers: sql`COUNT(au.id)`,
2004
+ totalCompletedOrders: sql`COUNT(co.id)`
2005
+ })
2006
+ .from(sql`activeUsers au`)
2007
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`))
2008
+ );
804
2009
  ```
805
2010
 
806
- This analysis helps you understand:
2011
+ This analysis provides insights into:
807
2012
  - How the database executes your query
808
2013
  - Which indexes are being used
809
2014
  - Estimated vs actual row counts
810
2015
  - Resource usage at each step
811
- - Potential performance bottlenecks
2016
+ - Performance optimization opportunities
2017
+
2018
+
2019
+ ## Migration Guide
2020
+
2021
+ ### Migrating from 2.0.x to 2.1.x
2022
+
2023
+ This section covers the breaking changes introduced in version 2.1.x and how to migrate your existing code.
2024
+
2025
+ #### 1. Method Renaming (BREAKING CHANGES)
2026
+
2027
+ **Removed Methods:**
2028
+ - `forgeSQL.modify()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2029
+ - `forgeSQL.crud()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2030
+
2031
+ **Migration Steps:**
2032
+
2033
+ 1. **Replace `modify()` calls:**
2034
+ ```typescript
2035
+ // ❌ Old (2.0.x) - NO LONGER WORKS
2036
+ await forgeSQL.modify().insert(Users, [userData]);
2037
+ await forgeSQL.modify().updateById(updateData, Users);
2038
+ await forgeSQL.modify().deleteById(1, Users);
2039
+
2040
+ // ✅ New (2.1.x) - REQUIRED
2041
+ await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2042
+ await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
2043
+ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
2044
+ ```
2045
+
2046
+ 2. **Replace `crud()` calls:**
2047
+ ```typescript
2048
+ // ❌ Old (2.0.x) - NO LONGER WORKS
2049
+ await forgeSQL.crud().insert(Users, [userData]);
2050
+ await forgeSQL.crud().updateById(updateData, Users);
2051
+ await forgeSQL.crud().deleteById(1, Users);
2052
+
2053
+ // ✅ New (2.1.x) - REQUIRED
2054
+ await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2055
+ await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
2056
+ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
2057
+ ```
2058
+
2059
+ #### 2. New API Methods
2060
+
2061
+ **New Methods Available:**
2062
+ - `forgeSQL.insert()` - Basic Drizzle operations
2063
+ - `forgeSQL.update()` - Basic Drizzle operations
2064
+ - `forgeSQL.delete()` - Basic Drizzle operations
2065
+ - `forgeSQL.insertAndEvictCache()` - Basic Drizzle operations with evict cache after execution
2066
+ - `forgeSQL.updateAndEvictCache()` - Basic Drizzle operations with evict cache after execution
2067
+ - `forgeSQL.deleteAndEvictCache()` - Basic Drizzle operations with evict cache after execution
2068
+ - `forgeSQL.selectFrom()` - All-column queries with field aliasing
2069
+ - `forgeSQL.selectDistinctFrom()` - Distinct all-column queries with field aliasing
2070
+ - `forgeSQL.selectCacheableFrom()` - All-column queries with field aliasing and caching
2071
+ - `forgeSQL.selectDistinctCacheableFrom()` - Distinct all-column queries with field aliasing and caching
2072
+ - `forgeSQL.execute()` - Raw SQL queries with local caching
2073
+ - `forgeSQL.executeCacheable()` - Raw SQL queries with local and global caching
2074
+ - `forgeSQL.with()` - Common Table Expressions (CTEs)
2075
+
2076
+ **Optional Migration:**
2077
+ You can optionally migrate to the new API methods for better performance and cache management:
2078
+
2079
+ ```typescript
2080
+ // ❌ Old approach (still works)
2081
+ await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2082
+
2083
+ // ✅ New approach (recommended for new code)
2084
+ await forgeSQL.insert(Users).values(userData);
2085
+ // or for versioned operations with cache management
2086
+ await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
2087
+
2088
+ // ✅ New query methods for better performance
2089
+ const users = await forgeSQL.selectFrom(Users)
2090
+ .where(eq(Users.active, true));
2091
+
2092
+ const usersDistinct = await forgeSQL.selectDistinctFrom(Users)
2093
+ .where(eq(Users.active, true));
2094
+
2095
+ const usersCacheable = await forgeSQL.selectCacheableFrom(Users)
2096
+ .where(eq(Users.active, true));
2097
+
2098
+ // ✅ Raw SQL execution with caching
2099
+ const rawUsers = await forgeSQL.execute(
2100
+ "SELECT * FROM users WHERE active = ?",
2101
+ [true]
2102
+ );
2103
+
2104
+ const cachedRawUsers = await forgeSQL.executeCacheable(
2105
+ "SELECT * FROM users WHERE active = ?",
2106
+ [true],
2107
+ 300
2108
+ );
2109
+
2110
+ // ✅ Common Table Expressions (CTEs)
2111
+ const userStats = await forgeSQL
2112
+ .with(
2113
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2114
+ forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2115
+ )
2116
+ .select({
2117
+ totalActiveUsers: sql`COUNT(au.id)`,
2118
+ totalCompletedOrders: sql`COUNT(co.id)`
2119
+ })
2120
+ .from(sql`activeUsers au`)
2121
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
2122
+ ```
2123
+
2124
+ #### 3. Automatic Migration Script
2125
+
2126
+ You can use a simple find-and-replace to migrate your code:
2127
+
2128
+ ```bash
2129
+ # Replace modify() calls
2130
+ find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.modify()/forgeSQL.modifyWithVersioning()/g'
2131
+
2132
+ # Replace crud() calls
2133
+ find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.modifyWithVersioning()/g'
2134
+ ```
2135
+
2136
+ #### 4. Breaking Changes
2137
+
2138
+ **Important:** The old methods (`modify()` and `crud()`) have been completely removed in version 2.1.x.
812
2139
 
2140
+ - ❌ **2.1.x**: Old methods are no longer available
2141
+ - ✅ **Migration Required**: You must update your code to use the new methods
813
2142
 
814
2143
  ## License
815
2144
  This project is licensed under the **MIT License**.