forge-sql-orm 2.1.11 → 2.1.13

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 (73) hide show
  1. package/README.md +800 -541
  2. package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
  3. package/dist/core/ForgeSQLAnalyseOperations.js +257 -0
  4. package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -0
  5. package/dist/core/ForgeSQLCacheOperations.js +172 -0
  6. package/dist/core/ForgeSQLCacheOperations.js.map +1 -0
  7. package/dist/core/ForgeSQLCrudOperations.js +349 -0
  8. package/dist/core/ForgeSQLCrudOperations.js.map +1 -0
  9. package/dist/core/ForgeSQLORM.d.ts +1 -1
  10. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  11. package/dist/core/ForgeSQLORM.js +1191 -0
  12. package/dist/core/ForgeSQLORM.js.map +1 -0
  13. package/dist/core/ForgeSQLQueryBuilder.js +77 -0
  14. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -0
  15. package/dist/core/ForgeSQLSelectOperations.js +81 -0
  16. package/dist/core/ForgeSQLSelectOperations.js.map +1 -0
  17. package/dist/core/SystemTables.js +258 -0
  18. package/dist/core/SystemTables.js.map +1 -0
  19. package/dist/index.js +30 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  22. package/dist/lib/drizzle/extensions/additionalActions.js +527 -0
  23. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -0
  24. package/dist/utils/cacheContextUtils.d.ts.map +1 -1
  25. package/dist/utils/cacheContextUtils.js +198 -0
  26. package/dist/utils/cacheContextUtils.js.map +1 -0
  27. package/dist/utils/cacheUtils.d.ts.map +1 -1
  28. package/dist/utils/cacheUtils.js +383 -0
  29. package/dist/utils/cacheUtils.js.map +1 -0
  30. package/dist/utils/forgeDriver.d.ts.map +1 -1
  31. package/dist/utils/forgeDriver.js +139 -0
  32. package/dist/utils/forgeDriver.js.map +1 -0
  33. package/dist/utils/forgeDriverProxy.js +68 -0
  34. package/dist/utils/forgeDriverProxy.js.map +1 -0
  35. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  36. package/dist/utils/metadataContextUtils.js +28 -0
  37. package/dist/utils/metadataContextUtils.js.map +1 -0
  38. package/dist/utils/requestTypeContextUtils.js +10 -0
  39. package/dist/utils/requestTypeContextUtils.js.map +1 -0
  40. package/dist/utils/sqlHints.js +52 -0
  41. package/dist/utils/sqlHints.js.map +1 -0
  42. package/dist/utils/sqlUtils.d.ts.map +1 -1
  43. package/dist/utils/sqlUtils.js +590 -0
  44. package/dist/utils/sqlUtils.js.map +1 -0
  45. package/dist/webtriggers/applyMigrationsWebTrigger.js +77 -0
  46. package/dist/webtriggers/applyMigrationsWebTrigger.js.map +1 -0
  47. package/dist/webtriggers/clearCacheSchedulerTrigger.js +83 -0
  48. package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -0
  49. package/dist/webtriggers/dropMigrationWebTrigger.js +54 -0
  50. package/dist/webtriggers/dropMigrationWebTrigger.js.map +1 -0
  51. package/dist/webtriggers/dropTablesMigrationWebTrigger.js +54 -0
  52. package/dist/webtriggers/dropTablesMigrationWebTrigger.js.map +1 -0
  53. package/dist/webtriggers/fetchSchemaWebTrigger.js +82 -0
  54. package/dist/webtriggers/fetchSchemaWebTrigger.js.map +1 -0
  55. package/dist/webtriggers/index.js +40 -0
  56. package/dist/webtriggers/index.js.map +1 -0
  57. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -1
  58. package/dist/webtriggers/slowQuerySchedulerTrigger.js +80 -0
  59. package/dist/webtriggers/slowQuerySchedulerTrigger.js.map +1 -0
  60. package/package.json +28 -23
  61. package/src/core/ForgeSQLAnalyseOperations.ts +3 -2
  62. package/src/core/ForgeSQLORM.ts +33 -27
  63. package/src/lib/drizzle/extensions/additionalActions.ts +11 -0
  64. package/src/utils/cacheContextUtils.ts +9 -6
  65. package/src/utils/cacheUtils.ts +28 -5
  66. package/src/utils/forgeDriver.ts +10 -6
  67. package/src/utils/metadataContextUtils.ts +1 -4
  68. package/src/utils/sqlUtils.ts +136 -125
  69. package/src/webtriggers/slowQuerySchedulerTrigger.ts +40 -33
  70. package/dist/ForgeSQLORM.js +0 -3896
  71. package/dist/ForgeSQLORM.js.map +0 -1
  72. package/dist/ForgeSQLORM.mjs +0 -3879
  73. package/dist/ForgeSQLORM.mjs.map +0 -1
package/README.md CHANGED
@@ -11,14 +11,14 @@
11
11
  [![Coverage Status](https://coveralls.io/repos/github/vzakharchenko/forge-sql-orm/badge.svg?branch=master)](https://coveralls.io/github/vzakharchenko/forge-sql-orm?branch=master)
12
12
  [![DeepScan grade](https://deepscan.io/api/teams/26652/projects/29272/branches/940614/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=26652&pid=29272&bid=940614)
13
13
 
14
-
15
14
  **Forge-SQL-ORM** is an ORM designed for working with [@forge/sql](https://developer.atlassian.com/platform/forge/storage-reference/sql-tutorial/) in **Atlassian Forge**. It is built on top of [Drizzle ORM](https://orm.drizzle.team) and provides advanced capabilities for working with relational databases inside Forge.
16
15
 
17
16
  ## Key Features
17
+
18
18
  - ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
19
19
  - ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
20
20
  - ✅ **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/) )
21
- - ✅ **Performance Monitoring**: Query execution metrics and analysis capabilities with automatic error analysis for timeout and OOM errors
21
+ - ✅ **Performance Monitoring**: Query execution metrics and analysis capabilities with automatic error analysis for timeout and OOM errors, plus scheduled slow query monitoring with execution plans
22
22
  - ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
23
23
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
24
24
  - ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
@@ -37,6 +37,7 @@
37
37
  ## Table of Contents
38
38
 
39
39
  ### 🚀 Getting Started
40
+
40
41
  - [Key Features](#key-features)
41
42
  - [Usage Approaches](#usage-approaches)
42
43
  - [Installation](#installation)
@@ -44,16 +45,19 @@
44
45
  - [Quick Start](#quick-start)
45
46
 
46
47
  ### 📖 Core Features
48
+
47
49
  - [Field Name Collision Prevention](#field-name-collision-prevention-in-complex-queries)
48
50
  - [Drizzle Usage with forge-sql-orm](#drizzle-usage-with-forge-sql-orm)
49
51
  - [Direct Drizzle Usage with Custom Driver](#direct-drizzle-usage-with-custom-driver)
50
52
 
51
53
  ### 🗄️ Database Operations
54
+
52
55
  - [Fetch Data](#fetch-data)
53
56
  - [Modify Operations](#modify-operations)
54
57
  - [SQL Utilities](#sql-utilities)
55
58
 
56
59
  ### ⚡ Caching System
60
+
57
61
  - [Setting Up Caching with @forge/kvs](#setting-up-caching-with-forgekvs-optional)
58
62
  - [Global Cache System (Level 2)](#global-cache-system-level-2)
59
63
  - [Cache Context Operations](#cache-context-operations)
@@ -62,18 +66,22 @@
62
66
  - [Manual Cache Management](#manual-cache-management)
63
67
 
64
68
  ### 🔒 Advanced Features
69
+
65
70
  - [Optimistic Locking](#optimistic-locking)
66
71
  - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
67
- - [Performance Monitoring](#performance-monitoring)
72
+ - [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
73
+ - [Slow Query Monitoring](#slow-query-monitoring) - Scheduled monitoring of slow queries with execution plans
68
74
  - [Date and Time Types](#date-and-time-types)
69
75
 
70
76
  ### 🛠️ Development Tools
77
+
71
78
  - [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
72
79
  - [Web Triggers for Migrations](#web-triggers-for-migrations)
73
80
  - [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
74
81
  - [Drop Migrations](#drop-migrations)
75
82
 
76
83
  ### 📚 Examples
84
+
77
85
  - [Simple Example](examples/forge-sql-orm-example-simple)
78
86
  - [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
79
87
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
@@ -84,17 +92,20 @@
84
92
  - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
85
93
 
86
94
  ### 📚 Reference
95
+
87
96
  - [ForgeSqlOrmOptions](#forgesqlormoptions)
88
97
  - [Migration Guide](#migration-guide)
89
98
 
90
99
  ## 🚀 Quick Navigation
91
100
 
92
101
  **New to Forge-SQL-ORM?** Start here:
102
+
93
103
  - [Quick Start](#quick-start) - Get up and running in 5 minutes
94
104
  - [Installation](#installation) - Complete setup guide
95
105
  - [Basic Usage Examples](#fetch-data) - Simple query examples
96
106
 
97
107
  **Looking for specific features?**
108
+
98
109
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
99
110
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
100
111
  - [Optimistic Locking](#optimistic-locking) - Data consistency
@@ -102,6 +113,7 @@
102
113
  - [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
103
114
 
104
115
  **Looking for practical examples?**
116
+
105
117
  - [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
106
118
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
107
119
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
@@ -110,24 +122,27 @@
110
122
 
111
123
  ## Usage Approaches
112
124
 
113
-
114
125
  ### 1. Full Forge-SQL-ORM Usage
126
+
115
127
  ```typescript
116
128
  import ForgeSQL from "forge-sql-orm";
117
129
  const forgeSQL = new ForgeSQL();
118
130
  ```
131
+
119
132
  Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
120
133
 
121
134
  ### 2. Direct Drizzle Usage
135
+
122
136
  ```typescript
123
137
  import { drizzle } from "drizzle-orm/mysql-proxy";
124
138
  import { forgeDriver } from "forge-sql-orm";
125
139
  const db = drizzle(forgeDriver);
126
140
  ```
127
- 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.
128
141
 
142
+ 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.
129
143
 
130
144
  ### 3. Local Cache Optimization
145
+
131
146
  ```typescript
132
147
  import ForgeSQL from "forge-sql-orm";
133
148
  const forgeSQL = new ForgeSQL();
@@ -135,28 +150,28 @@ const forgeSQL = new ForgeSQL();
135
150
  // Optimize repeated queries within a single invocation
136
151
  await forgeSQL.executeWithLocalContext(async () => {
137
152
  // Multiple queries here will benefit from local caching
138
- const users = await forgeSQL.select({ id: users.id, name: users.name })
139
- .from(users).where(eq(users.active, true));
140
-
153
+ const users = await forgeSQL
154
+ .select({ id: users.id, name: users.name })
155
+ .from(users)
156
+ .where(eq(users.active, true));
157
+
141
158
  // This query will use local cache (no database call)
142
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
143
- .from(users).where(eq(users.active, true));
144
-
145
- // Using new methods for better performance
146
- const usersFrom = await forgeSQL.selectFrom(users)
159
+ const cachedUsers = await forgeSQL
160
+ .select({ id: users.id, name: users.name })
161
+ .from(users)
147
162
  .where(eq(users.active, true));
148
-
163
+
164
+ // Using new methods for better performance
165
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
166
+
149
167
  // This will use local cache (no database call)
150
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
151
- .where(eq(users.active, true));
152
-
168
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
169
+
153
170
  // Raw SQL with local caching
154
- const rawUsers = await forgeSQL.execute(
155
- "SELECT id, name FROM users WHERE active = ?",
156
- [true]
157
- );
171
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
158
172
  });
159
173
  ```
174
+
160
175
  Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
161
176
 
162
177
  ## Field Name Collision Prevention in Complex Queries
@@ -166,6 +181,7 @@ When working with complex queries involving multiple tables (joins, inner joins,
166
181
  Forge-SQL-ORM provides two ways to handle this:
167
182
 
168
183
  ### Using Forge-SQL-ORM
184
+
169
185
  ```typescript
170
186
  import ForgeSQL from "forge-sql-orm";
171
187
 
@@ -173,12 +189,13 @@ const forgeSQL = new ForgeSQL();
173
189
 
174
190
  // Automatic field name collision prevention
175
191
  await forgeSQL
176
- .select({user: users, order: orders})
192
+ .select({ user: users, order: orders })
177
193
  .from(orders)
178
194
  .innerJoin(users, eq(orders.userId, users.id));
179
195
  ```
180
196
 
181
197
  ### Using Direct Drizzle
198
+
182
199
  ```typescript
183
200
  import { drizzle } from "drizzle-orm/mysql-proxy";
184
201
  import { forgeDriver, patchDbWithSelectAliased } from "forge-sql-orm";
@@ -187,18 +204,18 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
187
204
 
188
205
  // Manual field name collision prevention
189
206
  await db
190
- .selectAliased({user: users, order: orders})
207
+ .selectAliased({ user: users, order: orders })
191
208
  .from(orders)
192
209
  .innerJoin(users, eq(orders.userId, users.id));
193
210
  ```
194
211
 
195
212
  ### Important Notes
213
+
196
214
  - This is a specific behavior of Atlassian Forge SQL, not Drizzle ORM
197
215
  - For complex queries involving multiple tables, it's recommended to always specify select fields and avoid using `select()` without field selection
198
216
  - The solution automatically creates unique aliases for each field by prefixing them with the table name
199
217
  - This ensures that fields with the same name from different tables remain distinct in the query results
200
218
 
201
-
202
219
  ## Installation
203
220
 
204
221
  Forge-SQL-ORM is designed to work with @forge/sql and requires some additional setup to ensure compatibility within Atlassian Forge.
@@ -206,16 +223,35 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
206
223
  ✅ Step 1: Install Dependencies
207
224
 
208
225
  **Basic installation (without caching):**
226
+
209
227
  ```sh
210
228
  npm install forge-sql-orm @forge/sql drizzle-orm -S
211
229
  ```
212
230
 
213
231
  **With caching support:**
232
+
214
233
  ```sh
215
234
  npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
216
235
  ```
217
236
 
237
+ **⚠️ Important for UI-Kit projects:**
238
+
239
+ If you're installing `forge-sql-orm` in a UI-Kit project (projects using `@forge/react`), you may encounter peer dependency conflicts with `@types/react`. This is due to a conflict between `@types/react@18` (required by `@forge/react`) and `@types/react@19` (optional peer dependency from `drizzle-orm` via `bun-types`).
240
+
241
+ To resolve this, use the `--legacy-peer-deps` flag:
242
+
243
+ ```sh
244
+ # Basic installation for UI-Kit projects
245
+ npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
246
+
247
+ # With caching support for UI-Kit projects
248
+ npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
249
+ ```
250
+
251
+ **Note:** The `--legacy-peer-deps` flag tells npm to ignore peer dependency conflicts. This is safe in this case because `bun-types` is an optional peer dependency and doesn't affect the functionality of `forge-sql-orm` in Forge environments.
252
+
218
253
  This will:
254
+
219
255
  - Install Forge-SQL-ORM (the ORM for @forge/sql)
220
256
  - Install @forge/sql, the Forge database layer
221
257
  - Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
@@ -226,6 +262,7 @@ This will:
226
262
  ## Quick Start
227
263
 
228
264
  ### 1. Basic Setup
265
+
229
266
  ```typescript
230
267
  import ForgeSQL from "forge-sql-orm";
231
268
 
@@ -237,44 +274,49 @@ const users = await forgeSQL.select().from(users);
237
274
  ```
238
275
 
239
276
  ### 2. With Caching (Optional)
277
+
240
278
  ```typescript
241
279
  import ForgeSQL from "forge-sql-orm";
242
280
 
243
281
  // Initialize with caching
244
282
  const forgeSQL = new ForgeSQL({
245
283
  cacheEntityName: "cache",
246
- cacheTTL: 300
284
+ cacheTTL: 300,
247
285
  });
248
286
 
249
287
  // Cached query
250
- const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
251
- .from(users).where(eq(users.active, true));
288
+ const users = await forgeSQL
289
+ .selectCacheable({ id: users.id, name: users.name })
290
+ .from(users)
291
+ .where(eq(users.active, true));
252
292
  ```
253
293
 
254
294
  ### 3. Local Cache Optimization
295
+
255
296
  ```typescript
256
297
  // Optimize repeated queries within a single invocation
257
298
  await forgeSQL.executeWithLocalContext(async () => {
258
- const users = await forgeSQL.select({ id: users.id, name: users.name })
259
- .from(users).where(eq(users.active, true));
260
-
299
+ const users = await forgeSQL
300
+ .select({ id: users.id, name: users.name })
301
+ .from(users)
302
+ .where(eq(users.active, true));
303
+
261
304
  // This query will use local cache (no database call)
262
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
263
- .from(users).where(eq(users.active, true));
264
-
265
- // Using new methods for better performance
266
- const usersFrom = await forgeSQL.selectFrom(users)
305
+ const cachedUsers = await forgeSQL
306
+ .select({ id: users.id, name: users.name })
307
+ .from(users)
267
308
  .where(eq(users.active, true));
268
-
309
+
310
+ // Using new methods for better performance
311
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
312
+
269
313
  // Raw SQL with local caching
270
- const rawUsers = await forgeSQL.execute(
271
- "SELECT id, name FROM users WHERE active = ?",
272
- [true]
273
- );
314
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
274
315
  });
275
316
  ```
276
317
 
277
318
  ### 4. Resolver Performance Monitoring
319
+
278
320
  ```typescript
279
321
  // Resolver with performance monitoring
280
322
  resolver.define("fetch", async (req: Request) => {
@@ -283,22 +325,23 @@ resolver.define("fetch", async (req: Request) => {
283
325
  async () => {
284
326
  // Resolver logic with multiple queries
285
327
  const users = await forgeSQL.selectFrom(demoUsers);
286
- const orders = await forgeSQL.selectFrom(demoOrders)
328
+ const orders = await forgeSQL
329
+ .selectFrom(demoOrders)
287
330
  .where(eq(demoOrders.userId, demoUsers.id));
288
331
  return { users, orders };
289
332
  },
290
333
  async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
291
334
  const threshold = 500; // ms baseline for this resolver
292
-
335
+
293
336
  if (totalDbExecutionTime > threshold * 1.5) {
294
- console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
337
+ console.warn(
338
+ `[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
339
+ );
295
340
  await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
296
341
  } else if (totalDbExecutionTime > threshold) {
297
- console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
342
+ console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
298
343
  }
299
-
300
- console.log(`DB response size: ${totalResponseSize} bytes`);
301
- }
344
+ },
302
345
  );
303
346
  } catch (e) {
304
347
  const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
@@ -309,6 +352,7 @@ resolver.define("fetch", async (req: Request) => {
309
352
  ```
310
353
 
311
354
  ### 5. Next Steps
355
+
312
356
  - [Full Installation Guide](#installation) - Complete setup instructions
313
357
  - [Core Features](#core-features) - Learn about key capabilities
314
358
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
@@ -344,47 +388,44 @@ const db = forgeSQL.getDrizzleQueryBuilder();
344
388
  const users = await db.select().from(users);
345
389
 
346
390
  // Using new methods for enhanced functionality
347
- const usersFrom = await forgeSQL.selectFrom(users)
348
- .where(eq(users.active, true));
391
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
349
392
 
350
- const usersDistinct = await forgeSQL.selectDistinctFrom(users)
351
- .where(eq(users.active, true));
393
+ const usersDistinct = await forgeSQL.selectDistinctFrom(users).where(eq(users.active, true));
352
394
 
353
- const usersCacheable = await forgeSQL.selectCacheableFrom(users)
354
- .where(eq(users.active, true));
395
+ const usersCacheable = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
355
396
 
356
397
  // Raw SQL execution
357
- const rawUsers = await forgeSQL.execute(
358
- "SELECT * FROM users WHERE active = ?",
359
- [true]
360
- );
398
+ const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
361
399
 
362
400
  // Raw SQL with caching
401
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
363
402
  const cachedRawUsers = await forgeSQL.executeCacheable(
364
- "SELECT * FROM users WHERE active = ?",
365
- [true],
366
- 300
403
+ "SELECT * FROM `users` WHERE active = ?",
404
+ [true],
405
+ 300,
367
406
  );
368
407
 
369
408
  // Raw SQL with execution metadata and performance monitoring
370
409
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
371
410
  async () => {
372
411
  const users = await forgeSQL.selectFrom(usersTable);
373
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
412
+ const orders = await forgeSQL
413
+ .selectFrom(ordersTable)
414
+ .where(eq(ordersTable.userId, usersTable.id));
374
415
  return { users, orders };
375
416
  },
376
417
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
377
418
  const threshold = 500; // ms baseline for this resolver
378
-
419
+
379
420
  if (totalDbExecutionTime > threshold * 1.5) {
380
421
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
381
422
  await printQueriesWithPlan(); // Analyze and print query execution plans
382
423
  } else if (totalDbExecutionTime > threshold) {
383
424
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
384
425
  }
385
-
426
+
386
427
  console.log(`DB response size: ${totalResponseSize} bytes`);
387
- }
428
+ },
388
429
  );
389
430
 
390
431
  // DDL operations for schema modifications
@@ -403,25 +444,25 @@ await forgeSQL.executeDDLActions(async () => {
403
444
  SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
404
445
  WHERE AVG_LATENCY > 1000000
405
446
  `);
406
-
447
+
407
448
  // Execute complex analysis queries in DDL context
408
449
  const performanceData = await forgeSQL.execute(`
409
450
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
410
451
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
411
452
  `);
412
-
453
+
413
454
  return { slowQueries, performanceData };
414
455
  });
415
456
 
416
457
  // Common Table Expressions (CTEs)
417
458
  const userStats = await forgeSQL
418
459
  .with(
419
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
420
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
460
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
461
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
421
462
  )
422
463
  .select({
423
464
  totalActiveUsers: sql`COUNT(au.id)`,
424
- totalCompletedOrders: sql`COUNT(co.id)`
465
+ totalCompletedOrders: sql`COUNT(co.id)`,
425
466
  })
426
467
  .from(sql`activeUsers au`)
427
468
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -460,7 +501,7 @@ await forgeSQL.executeWithCacheContext(async () => {
460
501
  await db.updateWithCacheContext(users)...;
461
502
  await db.deleteWithCacheContext(users)...;
462
503
  // invoke without cache
463
- const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
504
+ const users = await db.selectAliasedCacheable(getTableColumns(users)).from(users);
464
505
  // Cache is cleared only once at the end for all affected tables
465
506
  });
466
507
 
@@ -476,14 +517,15 @@ const usersCacheable = await forgeSQL.selectCacheableFrom(users)
476
517
 
477
518
  // Raw SQL execution
478
519
  const rawUsers = await forgeSQL.execute(
479
- "SELECT * FROM users WHERE active = ?",
520
+ "SELECT * FROM users WHERE active = ?",
480
521
  [true]
481
522
  );
482
523
 
483
524
  // Raw SQL with caching
525
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
484
526
  const cachedRawUsers = await forgeSQL.executeCacheable(
485
- "SELECT * FROM users WHERE active = ?",
486
- [true],
527
+ "SELECT * FROM `users` WHERE active = ?",
528
+ [true],
487
529
  300
488
530
  );
489
531
 
@@ -496,14 +538,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
496
538
  },
497
539
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
498
540
  const threshold = 500; // ms baseline for this resolver
499
-
541
+
500
542
  if (totalDbExecutionTime > threshold * 1.5) {
501
543
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
502
544
  await printQueriesWithPlan(); // Analyze and print query execution plans
503
545
  } else if (totalDbExecutionTime > threshold) {
504
546
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
505
547
  }
506
-
548
+
507
549
  console.log(`DB response size: ${totalResponseSize} bytes`);
508
550
  }
509
551
  );
@@ -518,6 +560,7 @@ The caching system is optional and only needed if you want to use cache-related
518
560
  To use caching, you need to use Forge-SQL-ORM methods that support cache management:
519
561
 
520
562
  **Methods that perform cache eviction after execution and in cache context (batch eviction):**
563
+
521
564
  - `forgeSQL.insertAndEvictCache()`
522
565
  - `forgeSQL.updateAndEvictCache()`
523
566
  - `forgeSQL.deleteAndEvictCache()`
@@ -527,6 +570,7 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
527
570
  - `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
528
571
 
529
572
  **Methods that participate in cache context only (batch eviction):**
573
+
530
574
  - All methods except the default Drizzle methods:
531
575
  - `forgeSQL.insert()`
532
576
  - `forgeSQL.update()`
@@ -537,17 +581,20 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
537
581
  - `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
538
582
 
539
583
  **Methods do not do evict cache, better do not use with cache feature:**
540
- - `forgeSQL.getDrizzleQueryBuilder().insert()`
541
- - `forgeSQL.getDrizzleQueryBuilder().update()`
542
- - `forgeSQL.getDrizzleQueryBuilder().delete()`
584
+
585
+ - `forgeSQL.getDrizzleQueryBuilder().insert()`
586
+ - `forgeSQL.getDrizzleQueryBuilder().update()`
587
+ - `forgeSQL.getDrizzleQueryBuilder().delete()`
543
588
 
544
589
  **Cacheable methods:**
545
- - `forgeSQL.selectCacheable()`
546
- - `forgeSQL.selectDistinctCacheable()`
547
- - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
548
- - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
590
+
591
+ - `forgeSQL.selectCacheable()`
592
+ - `forgeSQL.selectDistinctCacheable()`
593
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
594
+ - `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
549
595
 
550
596
  **Cache context example:**
597
+
551
598
  ```typescript
552
599
  await forgeSQL.executeWithCacheContext(async () => {
553
600
  // These methods participate in batch cache clearing
@@ -558,7 +605,6 @@ await forgeSQL.executeWithCacheContext(async () => {
558
605
  });
559
606
  ```
560
607
 
561
-
562
608
  The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
563
609
 
564
610
  1. Resolver calls forge-sql-orm with a SQL query and parameters.
@@ -570,7 +616,6 @@ The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
570
616
 
571
617
  ![img.png](img/umlCache1.png)
572
618
 
573
-
574
619
  The diagram below shows how Evict Cache works in Forge-SQL-ORM:
575
620
 
576
621
  1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
@@ -604,19 +649,23 @@ The diagram below shows how Cache Context works:
604
649
 
605
650
  ![img.png](img/umlCacheEvictCacheContext1.png)
606
651
 
607
-
608
652
  ### Important Considerations
609
653
 
610
654
  **@forge/kvs Limits:**
611
655
  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.
612
656
 
613
657
  **Caching Guidelines:**
658
+
614
659
  - Don't cache everything - be selective about what to cache
615
660
  - Don't cache simple and fast queries - sometimes direct query is faster than cache
616
661
  - Consider data size and frequency of changes
617
662
  - Monitor cache usage to stay within quotas
618
663
  - Use appropriate TTL values
619
664
 
665
+ **⚠️ Important Cache Limitations:**
666
+
667
+ - **Table names starting with `a_`**: Tables whose names start with `a_` (case-insensitive) are automatically ignored in cache operations. KVS Cache will not work with such tables, and they will be excluded from cache invalidation and cache key generation.
668
+
620
669
  ### Step 1: Install Dependencies
621
670
 
622
671
  ```bash
@@ -653,18 +702,18 @@ modules:
653
702
  - key: clearCache
654
703
  handler: index.clearCache
655
704
  ```
705
+
656
706
  ```typescript
657
707
  // Example usage in your Forge app
658
708
  import { clearCacheSchedulerTrigger } from "forge-sql-orm";
659
709
 
660
710
  export const clearCache = () => {
661
- return clearCacheSchedulerTrigger({
711
+ return clearCacheSchedulerTrigger({
662
712
  cacheEntityName: "cache",
663
713
  });
664
714
  };
665
715
  ```
666
716
 
667
-
668
717
  ### Step 3: Configure ORM Options
669
718
 
670
719
  Set the cache entity name in your ForgeSQL configuration:
@@ -681,6 +730,7 @@ const forgeSQL = new ForgeSQL(options);
681
730
  ```
682
731
 
683
732
  **Important Notes:**
733
+
684
734
  - The `cacheEntityName` must exactly match the `name` in your manifest storage entities
685
735
  - The entity attributes (`sql`, `expiration`, `data`) are required for proper cache functionality
686
736
  - Indexes on `sql` and `expiration` improve cache lookup performance
@@ -692,11 +742,14 @@ const forgeSQL = new ForgeSQL(options);
692
742
  **Basic setup (without caching):**
693
743
 
694
744
  **package.json:**
745
+
695
746
  ```shell
696
747
  npm install forge-sql-orm @forge/sql drizzle-orm -S
748
+ # For UI-Kit projects, use: npm install forge-sql-orm @forge/sql drizzle-orm -S --legacy-peer-deps
697
749
  ```
698
750
 
699
751
  **manifest.yml:**
752
+
700
753
  ```yaml
701
754
  modules:
702
755
  sql:
@@ -705,6 +758,7 @@ modules:
705
758
  ```
706
759
 
707
760
  **index.ts:**
761
+
708
762
  ```typescript
709
763
  import ForgeSQL from "forge-sql-orm";
710
764
 
@@ -714,17 +768,18 @@ const forgeSQL = new ForgeSQL();
714
768
  await forgeSQL.insert(Users, [userData]);
715
769
  // Use versioned operations without caching
716
770
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
717
- const users = await forgeSQL.select({id: Users.id});
718
-
719
-
771
+ const users = await forgeSQL.select({ id: Users.id });
720
772
  ```
721
773
 
722
774
  **With caching support:**
775
+
723
776
  ```shell
724
777
  npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
778
+ # For UI-Kit projects, use: npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S --legacy-peer-deps
725
779
  ```
726
780
 
727
781
  **manifest.yml:**
782
+
728
783
  ```yaml
729
784
  modules:
730
785
  scheduledTrigger:
@@ -758,21 +813,23 @@ modules:
758
813
  import ForgeSQL from "forge-sql-orm";
759
814
 
760
815
  const forgeSQL = new ForgeSQL({
761
- cacheEntityName: "cache"
816
+ cacheEntityName: "cache",
762
817
  });
763
818
 
764
- import {clearCacheSchedulerTrigger} from "forge-sql-orm";
765
- import {getTableColumns} from "drizzle-orm";
819
+ import { clearCacheSchedulerTrigger } from "forge-sql-orm";
820
+ import { getTableColumns } from "drizzle-orm";
766
821
 
767
822
  export const clearCache = () => {
768
- return clearCacheSchedulerTrigger({
769
- cacheEntityName: "cache",
770
- });
823
+ return clearCacheSchedulerTrigger({
824
+ cacheEntityName: "cache",
825
+ });
771
826
  };
772
827
 
773
-
774
828
  // Now you can use caching features
775
- const usersData = await forgeSQL.selectCacheable(getTableColumns(users)).from(users).where(eq(users.active, true))
829
+ const usersData = await forgeSQL
830
+ .selectCacheable(getTableColumns(users))
831
+ .from(users)
832
+ .where(eq(users.active, true));
776
833
 
777
834
  // simple insert
778
835
  await forgeSQL.insertAndEvictCache(users, [userData]);
@@ -781,159 +838,194 @@ await forgeSQL.modifyWithVersioningAndEvictCache().insert(users, [userData]);
781
838
 
782
839
  // use Cache Context
783
840
  const data = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
784
- // after insert mark users to evict
785
- await forgeSQL.insert(users, [userData]);
786
- // after insertAndEvictCache mark orders to evict
787
- await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
788
- // execute query and put result to local cache
789
- await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
790
- .from(users)
791
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
792
- // use local cache without @forge/kvs and @forge/sql
793
- return await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
794
- .from(users)
795
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
796
- })
841
+ // after insert mark users to evict
842
+ await forgeSQL.insert(users, [userData]);
843
+ // after insertAndEvictCache mark orders to evict
844
+ await forgeSQL.insertAndEvictCache(orders, [order1, order2]);
845
+ // execute query and put result to local cache
846
+ await forgeSQL
847
+ .selectCacheable({
848
+ userId: users.id,
849
+ userName: users.name,
850
+ orderId: orders.id,
851
+ orderName: orders.name,
852
+ })
853
+ .from(users)
854
+ .innerJoin(orders, eq(orders.userId, users.id))
855
+ .where(eq(users.active, true));
856
+ // use local cache without @forge/kvs and @forge/sql
857
+ return await forgeSQL
858
+ .selectCacheable({
859
+ userId: users.id,
860
+ userName: users.name,
861
+ orderId: orders.id,
862
+ orderName: orders.name,
863
+ })
864
+ .from(users)
865
+ .innerJoin(orders, eq(orders.userId, users.id))
866
+ .where(eq(users.active, true));
867
+ });
797
868
  // execute query and put result to kvs cache
798
- await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
799
- .from(users)
800
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
869
+ await forgeSQL
870
+ .selectCacheable({
871
+ userId: users.id,
872
+ userName: users.name,
873
+ orderId: orders.id,
874
+ orderName: orders.name,
875
+ })
876
+ .from(users)
877
+ .innerJoin(orders, eq(orders.userId, users.id))
878
+ .where(eq(users.active, true));
801
879
 
802
880
  // get result from @foge/kvs cache without real @forge/sql call
803
- await forgeSQL.selectCacheable({userId: users.id, userName: users.name, orderId: orders.id, orderName: orders.name})
804
- .from(users)
805
- .innerJoin(orders, eq(orders.userId, users.id)).where(eq(users.active, true))
881
+ await forgeSQL
882
+ .selectCacheable({
883
+ userId: users.id,
884
+ userName: users.name,
885
+ orderId: orders.id,
886
+ orderName: orders.name,
887
+ })
888
+ .from(users)
889
+ .innerJoin(orders, eq(orders.userId, users.id))
890
+ .where(eq(users.active, true));
806
891
 
807
892
  // use Local Cache for performance optimization
808
893
  const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
809
- // First query - hits database and caches result
810
- const users = await forgeSQL.select({id: users.id, name: users.name})
811
- .from(users).where(eq(users.active, true));
812
-
813
- // Second query - uses local cache (no database call)
814
- const cachedUsers = await forgeSQL.select({id: users.id, name: users.name})
815
- .from(users).where(eq(users.active, true));
816
-
817
- // Using new methods for better performance
818
- const usersFrom = await forgeSQL.selectFrom(users)
819
- .where(eq(users.active, true));
820
-
821
- // This will use local cache (no database call)
822
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
823
- .where(eq(users.active, true));
824
-
825
- // Raw SQL with local caching
826
- const rawUsers = await forgeSQL.execute(
827
- "SELECT id, name FROM users WHERE active = ?",
828
- [true]
829
- );
830
-
831
- // Insert operation - evicts local cache
832
- await forgeSQL.insert(users).values({name: 'New User', active: true});
833
-
834
- // Third query - hits database again and caches new result
835
- const updatedUsers = await forgeSQL.select({id: users.id, name: users.name})
836
- .from(users).where(eq(users.active, true));
837
-
838
- return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
839
- });
894
+ // First query - hits database and caches result
895
+ const users = await forgeSQL
896
+ .select({ id: users.id, name: users.name })
897
+ .from(users)
898
+ .where(eq(users.active, true));
899
+
900
+ // Second query - uses local cache (no database call)
901
+ const cachedUsers = await forgeSQL
902
+ .select({ id: users.id, name: users.name })
903
+ .from(users)
904
+ .where(eq(users.active, true));
905
+
906
+ // Using new methods for better performance
907
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
908
+
909
+ // This will use local cache (no database call)
910
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
911
+
912
+ // Raw SQL with local caching
913
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
914
+
915
+ // Insert operation - evicts local cache
916
+ await forgeSQL.insert(users).values({ name: "New User", active: true });
917
+
918
+ // Third query - hits database again and caches new result
919
+ const updatedUsers = await forgeSQL
920
+ .select({ id: users.id, name: users.name })
921
+ .from(users)
922
+ .where(eq(users.active, true));
840
923
 
924
+ return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
925
+ });
841
926
  ```
842
927
 
843
928
  ## Choosing the Right Method - ForgeSQL ORM
844
929
 
845
930
  ### When to Use Each Approach
846
931
 
847
- | Method | Use Case | Versioning | Cache Management |
848
- |--------|----------|------------|------------------|
849
- | `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support| ✅ Yes | ✅ Yes |
850
- | `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
851
- | `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
852
- | `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
853
- | `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
854
- | `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
855
- | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
856
- | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
857
- | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
858
- | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
859
- | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
860
- | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
861
- | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
862
- | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
863
- | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
864
-
932
+ | Method | Use Case | Versioning | Cache Management |
933
+ | ------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
934
+ | `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support | ✅ Yes | ✅ Yes |
935
+ | `modifyWithVersioning()` | High-concurrency scenarios | ✅ Yes | Cache Context |
936
+ | `insertAndEvictCache()` | Simple inserts | ❌ No | ✅ Yes |
937
+ | `updateAndEvictCache()` | Simple updates | ❌ No | ✅ Yes |
938
+ | `deleteAndEvictCache()` | Simple deletes | ❌ No | ✅ Yes |
939
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | Cache Context |
940
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
941
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
942
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
943
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
944
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
945
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
946
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
947
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
948
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
865
949
 
866
950
  ## Choosing the Right Method - Direct Drizzle
867
951
 
868
952
  ### When to Use Each Approach
869
953
 
870
- | Method | Use Case | Versioning | Cache Management |
871
- |--------|----------|------------|------------------|
872
- | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
873
- | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
874
- | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
875
- | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
876
- | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
877
- | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
878
- | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
879
- | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
880
- | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
881
- | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
882
- | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
883
- | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
884
- | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
885
- | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
886
- | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
887
- where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
954
+ | Method | Use Case | Versioning | Cache Management |
955
+ | ---------------------------------------------------------------------- | ----------------------------------------------------------- | ---------- | -------------------- |
956
+ | `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
957
+ | `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
958
+ | `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
959
+ | `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
960
+ | `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
961
+ | `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
962
+ | `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
963
+ | `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
964
+ | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
965
+ | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
966
+ | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
967
+ | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
968
+ | `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
969
+ | `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
970
+ | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
888
971
 
972
+ where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
889
973
 
890
974
  ## Step-by-Step Migration Workflow
891
975
 
892
- 1. **Generate initial schema from an existing database**
976
+ 1. **Install CLI and setup scripts**
893
977
 
894
- ```sh
895
- npx forge-sql-orm-cli generate:model --dbName testDb --output ./database/schema
978
+ ```bash
979
+ npm install forge-sql-orm-cli -D
980
+ npm pkg set scripts.models:create="forge-sql-orm-cli generate:model --output src/entities --saveEnv"
981
+ npm pkg set scripts.migration:create="forge-sql-orm-cli migrations:create --force --output src/migration --entitiesPath src/entities"
982
+ npm pkg set scripts.migration:update="forge-sql-orm-cli migrations:update --entitiesPath src/entities --output src/migration"
896
983
  ```
897
984
 
898
985
  _(This is done only once when setting up the project)_
899
986
 
900
- 2. **Create the first migration**
987
+ 2. **Generate initial schema from an existing database**
901
988
 
902
989
  ```sh
903
- npx forge-sql-orm-cli migrations:create --dbName testDb --entitiesPath ./database/schema --output ./database/migration
990
+ npm run models:create
904
991
  ```
905
992
 
906
- _(This initializes the database migration structure, also done once)_
993
+ _(This will prompt for database credentials on first run and save them to `.env` file)_
994
+
995
+ 3. **Create the first migration**
996
+
997
+ ```sh
998
+ npm run migration:create
999
+ ```
907
1000
 
908
- 3. **Deploy to Forge and verify that migrations work**
1001
+ _(This initializes the database migration structure, also done once)_
909
1002
 
1003
+ 4. **Deploy to Forge and verify that migrations work**
910
1004
  - Deploy your **Forge app** with migrations.
911
1005
  - Run migrations using a **Forge web trigger** or **Forge scheduler**.
912
1006
 
913
- 4. **Modify the database (e.g., add a new column, index, etc.)**
914
-
1007
+ 5. **Modify the database (e.g., add a new column, index, etc.)**
915
1008
  - Use **DbSchema** or manually alter the database schema.
916
1009
 
917
- 5. **Update the migration**
1010
+ 6. **Update the migration**
918
1011
 
919
1012
  ```sh
920
- npx forge-sql-orm-cli migrations:update --dbName testDb --entitiesPath ./database/schema --output ./database/migration
1013
+ npm run migration:update
921
1014
  ```
922
1015
 
923
1016
  - ⚠️ **Do NOT update schema before this step!**
924
1017
  - If schema is updated first, the migration will be empty!
925
1018
 
926
- 6. **Deploy to Forge and verify that the migration runs without issues**
927
-
1019
+ 7. **Deploy to Forge and verify that the migration runs without issues**
928
1020
  - Run the updated migration on Forge.
929
1021
 
930
- 7. **Update the schema**
1022
+ 8. **Update the schema**
931
1023
 
932
1024
  ```sh
933
- npx forge-sql-orm-cli generate:model --dbName testDb --output ./database/schema
1025
+ npm run models:create
934
1026
  ```
935
1027
 
936
- 8. **Repeat steps 4-7 as needed**
1028
+ 9. **Repeat steps 5-8 as needed**
937
1029
 
938
1030
  **⚠️ WARNING:**
939
1031
 
@@ -943,6 +1035,7 @@ where Cache context - allows you to batch cache invalidation events and bypass c
943
1035
  ## Drop Migrations
944
1036
 
945
1037
  The Drop Migrations feature allows you to completely reset your database schema in Atlassian Forge SQL. This is useful when you need to:
1038
+
946
1039
  - Start fresh with a new schema
947
1040
  - Reset all tables and their data
948
1041
  - Clear migration history
@@ -951,6 +1044,7 @@ The Drop Migrations feature allows you to completely reset your database schema
951
1044
  ### Important Requirements
952
1045
 
953
1046
  Before using Drop Migrations, ensure that:
1047
+
954
1048
  1. Your local schema exactly matches the current database schema deployed in Atlassian Forge SQL
955
1049
  2. You have a backup of your data if needed
956
1050
  3. You understand that this operation will delete all tables and data
@@ -958,16 +1052,21 @@ Before using Drop Migrations, ensure that:
958
1052
  ### Usage
959
1053
 
960
1054
  1. First, ensure your local schema matches the deployed database:
1055
+
961
1056
  ```bash
962
- npx forge-sql-orm-cli generate:model --output ./database/schema
1057
+ npm run models:create
963
1058
  ```
964
1059
 
965
1060
  2. Generate the drop migration:
1061
+
966
1062
  ```bash
967
- npx forge-sql-orm-cli migrations:drop --entitiesPath ./database/schema --output ./database/migration
1063
+ npm run migration:drop
968
1064
  ```
969
1065
 
1066
+ _(Add this script to your package.json: `npm pkg set scripts.migration:drop="forge-sql-orm-cli migrations:drop --entitiesPath src/entities --output src/migration"`)_
1067
+
970
1068
  3. Deploy and run the migration in your Forge app:
1069
+
971
1070
  ```js
972
1071
  import migrationRunner from "./database/migration";
973
1072
  import { MigrationRunner } from "@forge/sql/out/migration";
@@ -979,13 +1078,14 @@ Before using Drop Migrations, ensure that:
979
1078
 
980
1079
  4. After dropping all tables, you can create a new migration to recreate the schema:
981
1080
  ```bash
982
- npx forge-sql-orm-cli migrations:create --entitiesPath ./database/schema --output ./database/migration --force
1081
+ npm run migration:create
983
1082
  ```
984
- The `--force` parameter is required here because we're creating a new migration after dropping all tables.
1083
+ The `--force` parameter is already included in the script to allow creating migrations after dropping all tables.
985
1084
 
986
1085
  ### Example Migration Output
987
1086
 
988
1087
  The generated drop migration will look like this:
1088
+
989
1089
  ```js
990
1090
  import { MigrationRunner } from "@forge/sql/out/migration";
991
1091
 
@@ -1013,31 +1113,36 @@ export default (migrationRunner: MigrationRunner): MigrationRunner => {
1013
1113
 
1014
1114
  When working with date and time fields in your models, you should use the custom types provided by Forge-SQL-ORM to ensure proper handling of date/time values. This is necessary because Forge SQL has specific format requirements for date/time values:
1015
1115
 
1016
- | Date type | Required Format | Example |
1017
- |-----------|----------------|---------|
1018
- | DATE | YYYY-MM-DD | 2024-09-19 |
1019
- | TIME | HH:MM:SS[.fraction] | 06:40:34 |
1116
+ | Date type | Required Format | Example |
1117
+ | --------- | ------------------------------ | -------------------------- |
1118
+ | DATE | YYYY-MM-DD | 2024-09-19 |
1119
+ | TIME | HH:MM:SS[.fraction] | 06:40:34 |
1020
1120
  | TIMESTAMP | YYYY-MM-DD HH:MM:SS[.fraction] | 2024-09-19 06:40:34.999999 |
1021
1121
 
1022
1122
  ```typescript
1023
1123
  // ❌ Don't use standard Drizzle date/time types
1024
- export const testEntityTimeStampVersion = mysqlTable('test_entity', {
1025
- id: int('id').primaryKey().autoincrement(),
1026
- time_stamp: timestamp('times_tamp').notNull(),
1027
- date_time: datetime('date_time').notNull(),
1028
- time: time('time').notNull(),
1029
- date: date('date').notNull(),
1124
+ export const testEntityTimeStampVersion = mysqlTable("test_entity", {
1125
+ id: int("id").primaryKey().autoincrement(),
1126
+ time_stamp: timestamp("times_tamp").notNull(),
1127
+ date_time: datetime("date_time").notNull(),
1128
+ time: time("time").notNull(),
1129
+ date: date("date").notNull(),
1030
1130
  });
1031
1131
 
1032
1132
  // ✅ Use Forge-SQL-ORM custom types instead
1033
- import { forgeDateTimeString, forgeDateString, forgeTimestampString, forgeTimeString } from 'forge-sql-orm'
1034
-
1035
- export const testEntityTimeStampVersion = mysqlTable('test_entity', {
1036
- id: int('id').primaryKey().autoincrement(),
1037
- time_stamp: forgeTimestampString('times_tamp').notNull(),
1038
- date_time: forgeDateTimeString('date_time').notNull(),
1039
- time: forgeTimeString('time').notNull(),
1040
- date: forgeDateString('date').notNull(),
1133
+ import {
1134
+ forgeDateTimeString,
1135
+ forgeDateString,
1136
+ forgeTimestampString,
1137
+ forgeTimeString,
1138
+ } from "forge-sql-orm";
1139
+
1140
+ export const testEntityTimeStampVersion = mysqlTable("test_entity", {
1141
+ id: int("id").primaryKey().autoincrement(),
1142
+ time_stamp: forgeTimestampString("times_tamp").notNull(),
1143
+ date_time: forgeDateTimeString("date_time").notNull(),
1144
+ time: forgeTimeString("time").notNull(),
1145
+ date: forgeDateString("date").notNull(),
1041
1146
  });
1042
1147
  ```
1043
1148
 
@@ -1053,6 +1158,7 @@ const timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS");
1053
1158
  ```
1054
1159
 
1055
1160
  Our custom types provide:
1161
+
1056
1162
  - Automatic conversion between JavaScript Date objects and Forge SQL's required string formats
1057
1163
  - Consistent date/time handling across your application
1058
1164
  - Type safety for date/time fields
@@ -1068,9 +1174,6 @@ Our custom types provide:
1068
1174
 
1069
1175
  Each type ensures that the data is properly formatted according to Forge SQL's requirements while providing a clean, type-safe interface for your application code.
1070
1176
 
1071
-
1072
-
1073
-
1074
1177
  # Connection to ORM
1075
1178
 
1076
1179
  ```js
@@ -1078,7 +1181,8 @@ import ForgeSQL from "forge-sql-orm";
1078
1181
 
1079
1182
  const forgeSQL = new ForgeSQL();
1080
1183
  ```
1081
- or
1184
+
1185
+ or
1082
1186
 
1083
1187
  ```typescript
1084
1188
  import { drizzle } from "drizzle-orm/mysql-proxy";
@@ -1097,69 +1201,47 @@ const users = await db.select().from(users);
1097
1201
 
1098
1202
  ```js
1099
1203
  // Using forgeSQL.select()
1100
- const user = await forgeSQL
1101
- .select({user: users})
1102
- .from(users);
1204
+ const user = await forgeSQL.select({ user: users }).from(users);
1103
1205
 
1104
1206
  // Using forgeSQL.selectDistinct()
1105
- const user = await forgeSQL
1106
- .selectDistinct({user: users})
1107
- .from(users);
1207
+ const user = await forgeSQL.selectDistinct({ user: users }).from(users);
1108
1208
 
1109
1209
  // Using forgeSQL.selectCacheable()
1110
- const user = await forgeSQL
1111
- .selectCacheable({user: users})
1112
- .from(users);
1210
+ const user = await forgeSQL.selectCacheable({ user: users }).from(users);
1113
1211
 
1114
1212
  // Using forgeSQL.selectFrom() - Select all columns with field aliasing
1115
- const user = await forgeSQL
1116
- .selectFrom(users)
1117
- .where(eq(users.id, 1));
1213
+ const user = await forgeSQL.selectFrom(users).where(eq(users.id, 1));
1118
1214
 
1119
1215
  // Using forgeSQL.selectDistinctFrom() - Select distinct all columns with field aliasing
1120
- const user = await forgeSQL
1121
- .selectDistinctFrom(users)
1122
- .where(eq(users.id, 1));
1216
+ const user = await forgeSQL.selectDistinctFrom(users).where(eq(users.id, 1));
1123
1217
 
1124
1218
  // Using forgeSQL.selectCacheableFrom() - Select all columns with field aliasing and caching
1125
- const user = await forgeSQL
1126
- .selectCacheableFrom(users)
1127
- .where(eq(users.id, 1));
1219
+ const user = await forgeSQL.selectCacheableFrom(users).where(eq(users.id, 1));
1128
1220
 
1129
1221
  // Using forgeSQL.selectDistinctCacheableFrom() - Select distinct all columns with field aliasing and caching
1130
- const user = await forgeSQL
1131
- .selectDistinctCacheableFrom(users)
1132
- .where(eq(users.id, 1));
1222
+ const user = await forgeSQL.selectDistinctCacheableFrom(users).where(eq(users.id, 1));
1133
1223
 
1134
1224
  // Using forgeSQL.execute() - Execute raw SQL with local caching
1135
- const user = await forgeSQL
1136
- .execute("SELECT * FROM users WHERE id = ?", [1]);
1225
+ const user = await forgeSQL.execute("SELECT * FROM users WHERE id = ?", [1]);
1137
1226
 
1138
1227
  // Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
1139
- const user = await forgeSQL
1140
- .executeCacheable("SELECT * FROM users WHERE id = ?", [1], 300);
1228
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
1229
+ // Example: SELECT * FROM `users` WHERE id = ? (NOT: SELECT * FROM users WHERE id = ?)
1230
+ const user = await forgeSQL.executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
1141
1231
 
1142
1232
  // Using forgeSQL.getDrizzleQueryBuilder()
1143
- const user = await forgeSQL
1144
- .getDrizzleQueryBuilder()
1145
- .select().from(Users)
1146
- .where(eq(Users.id, 1));
1233
+ const user = await forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1));
1147
1234
 
1148
1235
  // OR using direct drizzle with custom driver
1149
1236
  const db = drizzle(forgeDriver);
1150
- const user = await db
1151
- .select().from(Users)
1152
- .where(eq(Users.id, 1));
1237
+ const user = await db.select().from(Users).where(eq(Users.id, 1));
1153
1238
  // Returns: { id: 1, name: "John Doe" }
1154
1239
 
1155
1240
  // Using executeQueryOnlyOne for single result with error handling
1156
1241
  const user = await forgeSQL
1157
1242
  .fetch()
1158
1243
  .executeQueryOnlyOne(
1159
- forgeSQL
1160
- .getDrizzleQueryBuilder()
1161
- .select().from(Users)
1162
- .where(eq(Users.id, 1))
1244
+ forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1)),
1163
1245
  );
1164
1246
  // Returns: { id: 1, name: "John Doe" }
1165
1247
  // Throws error if multiple records found
@@ -1171,26 +1253,29 @@ const usersAlias = alias(Users, "u");
1171
1253
  const result = await forgeSQL
1172
1254
  .getDrizzleQueryBuilder()
1173
1255
  .select({
1174
- userId: sql<string>`${usersAlias.id} as \`userId\``,
1175
- userName: sql<string>`${usersAlias.name} as \`userName\``
1176
- }).from(usersAlias);
1256
+ userId: sql < string > `${usersAlias.id} as \`userId\``,
1257
+ userName: sql < string > `${usersAlias.name} as \`userName\``,
1258
+ })
1259
+ .from(usersAlias);
1177
1260
 
1178
1261
  // OR with direct drizzle
1179
1262
  const db = drizzle(forgeDriver);
1180
1263
  const result = await db
1181
1264
  .select({
1182
- userId: sql<string>`${usersAlias.id} as \`userId\``,
1183
- userName: sql<string>`${usersAlias.name} as \`userName\``
1184
- }).from(usersAlias);
1265
+ userId: sql < string > `${usersAlias.id} as \`userId\``,
1266
+ userName: sql < string > `${usersAlias.name} as \`userName\``,
1267
+ })
1268
+ .from(usersAlias);
1185
1269
  // Returns: { userId: 1, userName: "John Doe" }
1186
1270
  ```
1187
1271
 
1188
1272
  ### Complex Queries
1273
+
1189
1274
  ```js
1190
1275
  // Using joins with automatic field name collision prevention
1191
1276
  // With forgeSQL
1192
1277
  const orderWithUser = await forgeSQL
1193
- .select({user: users, order: orders})
1278
+ .select({ user: users, order: orders })
1194
1279
  .from(orders)
1195
1280
  .innerJoin(users, eq(orders.userId, users.id));
1196
1281
 
@@ -1209,12 +1294,12 @@ const orderWithUser = await forgeSQL
1209
1294
  // Using with() for Common Table Expressions (CTEs)
1210
1295
  const userStats = await forgeSQL
1211
1296
  .with(
1212
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1213
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1297
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
1298
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
1214
1299
  )
1215
1300
  .select({
1216
1301
  totalActiveUsers: sql`COUNT(au.id)`,
1217
- totalCompletedOrders: sql`COUNT(co.id)`
1302
+ totalCompletedOrders: sql`COUNT(co.id)`,
1218
1303
  })
1219
1304
  .from(sql`activeUsers au`)
1220
1305
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -1222,33 +1307,30 @@ const userStats = await forgeSQL
1222
1307
  // OR with direct drizzle
1223
1308
  const db = patchDbWithSelectAliased(drizzle(forgeDriver));
1224
1309
  const orderWithUser = await db
1225
- .selectAliased({user: users, order: orders})
1310
+ .selectAliased({ user: users, order: orders })
1226
1311
  .from(orders)
1227
1312
  .innerJoin(users, eq(orders.userId, users.id));
1228
- // Returns: {
1229
- // user_id: 1,
1313
+ // Returns: {
1314
+ // user_id: 1,
1230
1315
  // user_name: "John Doe",
1231
1316
  // order_id: 1,
1232
1317
  // order_product: "Product 1"
1233
1318
  // }
1234
1319
 
1235
1320
  // Using distinct with aliases
1236
- const uniqueUsers = await db
1237
- .selectAliasedDistinct({user: users})
1238
- .from(users);
1321
+ const uniqueUsers = await db.selectAliasedDistinct({ user: users }).from(users);
1239
1322
  // Returns unique users with aliased fields
1240
1323
 
1241
1324
  // Using executeQueryOnlyOne for unique results
1242
- const userStats = await forgeSQL
1243
- .fetch()
1244
- .executeQueryOnlyOne(
1245
- forgeSQL
1246
- .getDrizzleQueryBuilder()
1247
- .select({
1248
- totalUsers: sql`COUNT(*) as \`totalUsers\``,
1249
- uniqueNames: sql`COUNT(DISTINCT name) as \`uniqueNames\``
1250
- }).from(Users)
1251
- );
1325
+ const userStats = await forgeSQL.fetch().executeQueryOnlyOne(
1326
+ forgeSQL
1327
+ .getDrizzleQueryBuilder()
1328
+ .select({
1329
+ totalUsers: sql`COUNT(*) as \`totalUsers\``,
1330
+ uniqueNames: sql`COUNT(DISTINCT name) as \`uniqueNames\``,
1331
+ })
1332
+ .from(Users),
1333
+ );
1252
1334
  // Returns: { totalUsers: 100, uniqueNames: 80 }
1253
1335
  // Throws error if multiple records found
1254
1336
  ```
@@ -1266,8 +1348,10 @@ const users = await forgeSQL
1266
1348
  .execute("SELECT * FROM users WHERE active = ?", [true]);
1267
1349
 
1268
1350
  // Using executeCacheable() for raw SQL with local and global caching
1351
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
1352
+ // Example: SELECT * FROM `users` WHERE active = ? (NOT: SELECT * FROM users WHERE active = ?)
1269
1353
  const users = await forgeSQL
1270
- .executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
1354
+ .executeCacheable("SELECT * FROM `users` WHERE active = ?", [true], 300);
1271
1355
 
1272
1356
  // Using executeWithMetadata() for capturing execution metrics and performance monitoring
1273
1357
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
@@ -1278,14 +1362,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1278
1362
  },
1279
1363
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1280
1364
  const threshold = 500; // ms baseline for this resolver
1281
-
1365
+
1282
1366
  if (totalDbExecutionTime > threshold * 1.5) {
1283
1367
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1284
1368
  await printQueriesWithPlan(); // Analyze and print query execution plans
1285
1369
  } else if (totalDbExecutionTime > threshold) {
1286
1370
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1287
1371
  }
1288
-
1372
+
1289
1373
  console.log(`DB response size: ${totalResponseSize} bytes`);
1290
1374
  }
1291
1375
  );
@@ -1300,7 +1384,7 @@ await forgeSQL.executeDDL(`
1300
1384
  `);
1301
1385
 
1302
1386
  await forgeSQL.executeDDL(sql`
1303
- ALTER TABLE users
1387
+ ALTER TABLE users
1304
1388
  ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1305
1389
  `);
1306
1390
 
@@ -1311,23 +1395,23 @@ await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
1311
1395
  await forgeSQL.executeDDLActions(async () => {
1312
1396
  // Execute regular SQL queries in DDL context for performance monitoring
1313
1397
  const slowQueries = await forgeSQL.execute(`
1314
- SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1398
+ SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1315
1399
  WHERE AVG_LATENCY > 1000000
1316
1400
  `);
1317
-
1401
+
1318
1402
  // Execute complex analysis queries in DDL context
1319
1403
  const performanceData = await forgeSQL.execute(`
1320
1404
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
1321
1405
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
1322
1406
  `);
1323
-
1407
+
1324
1408
  return { slowQueries, performanceData };
1325
1409
  });
1326
1410
 
1327
1411
  // Using execute() with complex queries
1328
1412
  const userStats = await forgeSQL
1329
1413
  .execute(`
1330
- SELECT
1414
+ SELECT
1331
1415
  u.id,
1332
1416
  u.name,
1333
1417
  COUNT(o.id) as order_count,
@@ -1352,13 +1436,10 @@ These operations work like standard Drizzle methods but participate in cache con
1352
1436
  await forgeSQL.insert(Users).values({ id: 1, name: "Smith" });
1353
1437
 
1354
1438
  // Basic update (participates in cache context when used within executeWithCacheContext)
1355
- await forgeSQL.update(Users)
1356
- .set({ name: "Smith Updated" })
1357
- .where(eq(Users.id, 1));
1439
+ await forgeSQL.update(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
1358
1440
 
1359
1441
  // Basic delete (participates in cache context when used within executeWithCacheContext)
1360
- await forgeSQL.delete(Users)
1361
- .where(eq(Users.id, 1));
1442
+ await forgeSQL.delete(Users).where(eq(Users.id, 1));
1362
1443
  ```
1363
1444
 
1364
1445
  ### 2. Non-Versioned Operations with Cache Management
@@ -1370,13 +1451,10 @@ These operations don't use optimistic locking but provide cache invalidation:
1370
1451
  await forgeSQL.insertAndEvictCache(Users).values({ id: 1, name: "Smith" });
1371
1452
 
1372
1453
  // Update without versioning but with cache invalidation
1373
- await forgeSQL.updateAndEvictCache(Users)
1374
- .set({ name: "Smith Updated" })
1375
- .where(eq(Users.id, 1));
1454
+ await forgeSQL.updateAndEvictCache(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
1376
1455
 
1377
1456
  // Delete without versioning but with cache invalidation
1378
- await forgeSQL.deleteAndEvictCache(Users)
1379
- .where(eq(Users.id, 1));
1457
+ await forgeSQL.deleteAndEvictCache(Users).where(eq(Users.id, 1));
1380
1458
  ```
1381
1459
 
1382
1460
  ### 3. Versioned Operations with Cache Management (Recommended)
@@ -1385,16 +1463,20 @@ These operations use optimistic locking and automatic cache invalidation:
1385
1463
 
1386
1464
  ```js
1387
1465
  // Insert with versioning and cache management
1388
- const userId = await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [{ id: 1, name: "Smith" }]);
1466
+ const userId = await forgeSQL
1467
+ .modifyWithVersioningAndEvictCache()
1468
+ .insert(Users, [{ id: 1, name: "Smith" }]);
1389
1469
 
1390
1470
  // Bulk insert with versioning
1391
1471
  await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [
1392
- { id: 2, name: "Smith" },
1393
- { id: 3, name: "Vasyl" },
1394
- ]);
1472
+ { id: 2, name: "Smith" },
1473
+ { id: 3, name: "Vasyl" },
1474
+ ]);
1395
1475
 
1396
1476
  // Update by ID with optimistic locking and cache invalidation
1397
- await forgeSQL.modifyWithVersioningAndEvictCache().updateById({ id: 1, name: "Smith Updated" }, Users);
1477
+ await forgeSQL
1478
+ .modifyWithVersioningAndEvictCache()
1479
+ .updateById({ id: 1, name: "Smith Updated" }, Users);
1398
1480
 
1399
1481
  // Delete by ID with versioning and cache invalidation
1400
1482
  await forgeSQL.modifyWithVersioningAndEvictCache().deleteById(1, Users);
@@ -1438,18 +1520,16 @@ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
1438
1520
  import { nextVal } from "forge-sql-orm";
1439
1521
 
1440
1522
  const user = {
1441
- id: nextVal('user_id_seq'),
1523
+ id: nextVal("user_id_seq"),
1442
1524
  name: "user test",
1443
- organization_id: 1
1525
+ organization_id: 1,
1444
1526
  };
1445
1527
  const id = await forgeSQL.modifyWithVersioning().insert(appUser, [user]);
1446
1528
 
1447
1529
  // Update with custom WHERE condition
1448
- await forgeSQL.modifyWithVersioning().updateFields(
1449
- { name: "New Name", age: 35 },
1450
- Users,
1451
- eq(Users.email, "smith@example.com")
1452
- );
1530
+ await forgeSQL
1531
+ .modifyWithVersioning()
1532
+ .updateFields({ name: "New Name", age: 35 }, Users, eq(Users.email, "smith@example.com"));
1453
1533
 
1454
1534
  // Insert with duplicate handling
1455
1535
  await forgeSQL.modifyWithVersioning().insert(
@@ -1458,7 +1538,7 @@ await forgeSQL.modifyWithVersioning().insert(
1458
1538
  { id: 4, name: "Smith" },
1459
1539
  { id: 4, name: "Vasyl" },
1460
1540
  ],
1461
- true
1541
+ true,
1462
1542
  );
1463
1543
  ```
1464
1544
 
@@ -1480,19 +1560,21 @@ const result = await forgeSQL
1480
1560
  .offset(formatLimitOffset(350000));
1481
1561
 
1482
1562
  // The generated SQL will be:
1483
- // SELECT * FROM order_item
1484
- // ORDER BY created_at ASC
1485
- // LIMIT 10
1563
+ // SELECT * FROM order_item
1564
+ // ORDER BY created_at ASC
1565
+ // LIMIT 10
1486
1566
  // OFFSET 350000
1487
1567
  ```
1488
1568
 
1489
1569
  **Important Notes:**
1570
+
1490
1571
  - The function performs type checking to prevent SQL injection
1491
1572
  - It throws an error if the input is not a valid number
1492
1573
  - Use this function instead of direct parameter binding for LIMIT and OFFSET clauses
1493
1574
  - The function is specifically designed to work with Atlassian Forge SQL's limitations
1494
1575
 
1495
1576
  **Security Considerations:**
1577
+
1496
1578
  - The function includes validation to ensure the input is a valid number
1497
1579
  - This prevents SQL injection by ensuring only numeric values are inserted
1498
1580
  - Always use this function instead of string concatenation for LIMIT and OFFSET values
@@ -1526,9 +1608,9 @@ const options = {
1526
1608
  tableName: "users",
1527
1609
  versionField: {
1528
1610
  fieldName: "updatedAt",
1529
- }
1530
- }
1531
- }
1611
+ },
1612
+ },
1613
+ },
1532
1614
  };
1533
1615
 
1534
1616
  const forgeSQL = new ForgeSQL(options);
@@ -1551,7 +1633,6 @@ The caching system leverages Forge's Custom entity store to provide:
1551
1633
  // Value: { data: [...], expiration: 1234567890, sql: "select * from 1" }
1552
1634
  ```
1553
1635
 
1554
-
1555
1636
  ### Cache Context Operations
1556
1637
 
1557
1638
  The cache context allows you to batch cache invalidation events and bypass cache reads for affected tables:
@@ -1612,72 +1693,78 @@ Local cache is an in-memory caching layer that operates within a single resolver
1612
1693
  // Execute operations within a local cache context
1613
1694
  await forgeSQL.executeWithLocalContext(async () => {
1614
1695
  // First call - executes query and caches result
1615
- const users = await forgeSQL.select({ id: users.id, name: users.name })
1616
- .from(users).where(eq(users.active, true));
1617
-
1696
+ const users = await forgeSQL
1697
+ .select({ id: users.id, name: users.name })
1698
+ .from(users)
1699
+ .where(eq(users.active, true));
1700
+
1618
1701
  // Second call - gets result from local cache (no database query)
1619
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1620
- .from(users).where(eq(users.active, true));
1621
-
1622
- // Using new selectFrom methods with local caching
1623
- const usersFrom = await forgeSQL.selectFrom(users)
1702
+ const cachedUsers = await forgeSQL
1703
+ .select({ id: users.id, name: users.name })
1704
+ .from(users)
1624
1705
  .where(eq(users.active, true));
1625
-
1706
+
1707
+ // Using new selectFrom methods with local caching
1708
+ const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
1709
+
1626
1710
  // This will use local cache (no database call)
1627
- const cachedUsersFrom = await forgeSQL.selectFrom(users)
1628
- .where(eq(users.active, true));
1629
-
1711
+ const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
1712
+
1630
1713
  // Using execute() with local caching
1631
- const rawUsers = await forgeSQL.execute(
1632
- "SELECT id, name FROM users WHERE active = ?",
1633
- [true]
1634
- );
1635
-
1714
+ const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
1715
+
1636
1716
  // This will use local cache (no database call)
1637
- const cachedRawUsers = await forgeSQL.execute(
1638
- "SELECT id, name FROM users WHERE active = ?",
1639
- [true]
1640
- );
1641
-
1717
+ const cachedRawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [
1718
+ true,
1719
+ ]);
1720
+
1642
1721
  // Raw SQL with execution metadata and performance monitoring
1643
1722
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1644
1723
  async () => {
1645
1724
  const users = await forgeSQL.selectFrom(usersTable);
1646
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1725
+ const orders = await forgeSQL
1726
+ .selectFrom(ordersTable)
1727
+ .where(eq(ordersTable.userId, usersTable.id));
1647
1728
  return { users, orders };
1648
1729
  },
1649
1730
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1650
1731
  const threshold = 500; // ms baseline for this resolver
1651
-
1732
+
1652
1733
  if (totalDbExecutionTime > threshold * 1.5) {
1653
1734
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1654
1735
  await printQueriesWithPlan(); // Analyze and print query execution plans
1655
1736
  } else if (totalDbExecutionTime > threshold) {
1656
1737
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1657
1738
  }
1658
-
1739
+
1659
1740
  console.log(`DB response size: ${totalResponseSize} bytes`);
1660
- }
1741
+ },
1661
1742
  );
1662
-
1743
+
1663
1744
  // Insert operation - evicts local cache for users table
1664
- await forgeSQL.insert(users).values({ name: 'New User', active: true });
1665
-
1745
+ await forgeSQL.insert(users).values({ name: "New User", active: true });
1746
+
1666
1747
  // Third call - executes query again and caches new result
1667
- const updatedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1668
- .from(users).where(eq(users.active, true));
1748
+ const updatedUsers = await forgeSQL
1749
+ .select({ id: users.id, name: users.name })
1750
+ .from(users)
1751
+ .where(eq(users.active, true));
1669
1752
  });
1670
1753
 
1671
1754
  // Execute with return value
1672
1755
  const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1673
1756
  // First call - executes query and caches result
1674
- const users = await forgeSQL.select({ id: users.id, name: users.name })
1675
- .from(users).where(eq(users.active, true));
1676
-
1757
+ const users = await forgeSQL
1758
+ .select({ id: users.id, name: users.name })
1759
+ .from(users)
1760
+ .where(eq(users.active, true));
1761
+
1677
1762
  // Second call - gets result from local cache (no database query)
1678
- const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
1679
- .from(users).where(eq(users.active, true));
1680
-
1763
+ const cachedUsers = await forgeSQL
1764
+ .select({ id: users.id, name: users.name })
1765
+ .from(users)
1766
+ .where(eq(users.active, true));
1767
+
1681
1768
  return { users, cachedUsers };
1682
1769
  });
1683
1770
  ```
@@ -1689,57 +1776,57 @@ const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async (
1689
1776
  const userResolver = async (req) => {
1690
1777
  return await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
1691
1778
  // Get user details using selectFrom (all columns with field aliasing)
1692
- const user = await forgeSQL.selectFrom(users)
1693
- .where(eq(users.id, args.userId));
1694
-
1779
+ const user = await forgeSQL.selectFrom(users).where(eq(users.id, args.userId));
1780
+
1695
1781
  // Get user's orders using selectCacheableFrom (with caching)
1696
- const orders = await forgeSQL.selectCacheableFrom(orders)
1697
- .where(eq(orders.userId, args.userId));
1698
-
1782
+ const orders = await forgeSQL.selectCacheableFrom(orders).where(eq(orders.userId, args.userId));
1783
+
1699
1784
  // Get user's profile using raw SQL with execute()
1700
1785
  const profile = await forgeSQL.execute(
1701
- "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1702
- [args.userId]
1786
+ "SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
1787
+ [args.userId],
1703
1788
  );
1704
-
1789
+
1705
1790
  // Get user statistics using complex raw SQL
1706
- const stats = await forgeSQL.execute(`
1791
+ const stats = await forgeSQL.execute(
1792
+ `
1707
1793
  SELECT
1708
1794
  COUNT(o.id) as total_orders,
1709
1795
  SUM(o.amount) as total_spent,
1710
1796
  AVG(o.amount) as avg_order_value
1711
1797
  FROM orders o
1712
1798
  WHERE o.user_id = ? AND o.status = 'completed'
1713
- `, [args.userId]);
1714
-
1799
+ `,
1800
+ [args.userId],
1801
+ );
1802
+
1715
1803
  // If any of these queries are repeated within the same resolver,
1716
1804
  // they will use the local cache instead of hitting the database
1717
-
1805
+
1718
1806
  return {
1719
1807
  ...user[0],
1720
1808
  orders,
1721
1809
  profile: profile[0],
1722
- stats: stats[0]
1810
+ stats: stats[0],
1723
1811
  };
1724
1812
  });
1725
1813
  };
1726
1814
  ```
1727
1815
 
1728
-
1729
1816
  #### Local Cache (Level 1) vs Global Cache (Level 2)
1730
1817
 
1731
- | Feature | Local Cache (Level 1) | Global Cache (Level 2) |
1732
- |---------|----------------------|------------------------|
1733
- | **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
1734
- | **Scope** | Single forge invocation | Cross-invocation (between calls) |
1735
- | **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
1736
- | **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
1737
- | **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
1738
- | **Use Case** | Invocation optimization | Cross-invocation data sharing |
1739
- | **Configuration** | None required | Requires KVS setup |
1740
- | **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
1741
- | **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
1742
- | **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
1818
+ | Feature | Local Cache (Level 1) | Global Cache (Level 2) |
1819
+ | ------------------ | ------------------------------------- | ------------------------------------------- |
1820
+ | **Storage** | In-memory (Node.js process) | Persistent (KVS Custom Entities) |
1821
+ | **Scope** | Single forge invocation | Cross-invocation (between calls) |
1822
+ | **Persistence** | No (cleared on invocation end) | Yes (survives app redeploy) |
1823
+ | **Performance** | Very fast (memory access) | Fast (KVS optimized storage) |
1824
+ | **Memory Usage** | Low (invocation-scoped) | Higher (persistent storage) |
1825
+ | **Use Case** | Invocation optimization | Cross-invocation data sharing |
1826
+ | **Configuration** | None required | Requires KVS setup |
1827
+ | **TTL Support** | No (invocation-scoped) | Yes (automatic expiration) |
1828
+ | **Cache Eviction** | Automatic on DML operations | Manual or scheduled cleanup |
1829
+ | **Best For** | Repeated queries in single invocation | Frequently accessed data across invocations |
1743
1830
 
1744
1831
  #### Integration with Global Cache (Level 2)
1745
1832
 
@@ -1752,18 +1839,20 @@ await forgeSQL.executeWithLocalContext(async () => {
1752
1839
  // 1. Local cache (Level 1 - in-memory)
1753
1840
  // 2. Global cache (Level 2 - KVS)
1754
1841
  // 3. Database query
1755
- const users = await forgeSQL.selectCacheable({ id: users.id, name: users.name })
1756
- .from(users).where(eq(users.active, true));
1757
-
1758
- // Using new methods with multi-level caching
1759
- const usersFrom = await forgeSQL.selectCacheableFrom(users)
1842
+ const users = await forgeSQL
1843
+ .selectCacheable({ id: users.id, name: users.name })
1844
+ .from(users)
1760
1845
  .where(eq(users.active, true));
1761
-
1846
+
1847
+ // Using new methods with multi-level caching
1848
+ const usersFrom = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
1849
+
1762
1850
  // Raw SQL with multi-level caching
1851
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1763
1852
  const rawUsers = await forgeSQL.executeCacheable(
1764
- "SELECT id, name FROM users WHERE active = ?",
1765
- [true],
1766
- 300 // TTL in seconds
1853
+ "SELECT id, name FROM `users` WHERE active = ?",
1854
+ [true],
1855
+ 300, // TTL in seconds
1767
1856
  );
1768
1857
  });
1769
1858
  ```
@@ -1787,44 +1876,45 @@ The diagram below shows how local cache works in Forge-SQL-ORM:
1787
1876
  // Execute queries with caching
1788
1877
  const users = await forgeSQL.modifyWithVersioningAndEvictCache().executeQuery(
1789
1878
  forgeSQL.select().from(Users).where(eq(Users.active, true)),
1790
- 600 // Custom TTL in seconds
1879
+ 600, // Custom TTL in seconds
1791
1880
  );
1792
1881
 
1793
1882
  // Execute single result queries with caching
1794
- const user = await forgeSQL.modifyWithVersioningAndEvictCache().executeQueryOnlyOne(
1795
- forgeSQL.select().from(Users).where(eq(Users.id, 1))
1796
- );
1883
+ const user = await forgeSQL
1884
+ .modifyWithVersioningAndEvictCache()
1885
+ .executeQueryOnlyOne(forgeSQL.select().from(Users).where(eq(Users.id, 1)));
1797
1886
 
1798
1887
  // Execute raw SQL with caching
1799
1888
  const results = await forgeSQL.modifyWithVersioningAndEvictCache().executeRawSQL(
1800
1889
  "SELECT * FROM users WHERE active = ?",
1801
1890
  [true],
1802
- 300 // TTL in seconds
1891
+ 300, // TTL in seconds
1803
1892
  );
1804
1893
 
1805
1894
  // Using new methods for cache-aware operations
1806
- const usersFrom = await forgeSQL.selectCacheableFrom(Users)
1807
- .where(eq(Users.active, true));
1895
+ const usersFrom = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
1808
1896
 
1809
- const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1897
+ const usersDistinct = await forgeSQL
1898
+ .selectDistinctCacheableFrom(Users)
1810
1899
  .where(eq(Users.active, true));
1811
1900
 
1812
1901
  // Raw SQL with local and global caching
1902
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1813
1903
  const rawUsers = await forgeSQL.executeCacheable(
1814
- "SELECT * FROM users WHERE active = ?",
1904
+ "SELECT * FROM `users` WHERE active = ?",
1815
1905
  [true],
1816
- 300 // TTL in seconds
1906
+ 300, // TTL in seconds
1817
1907
  );
1818
1908
 
1819
1909
  // Using with() for Common Table Expressions with caching
1820
1910
  const userStats = await forgeSQL
1821
1911
  .with(
1822
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
1823
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
1912
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
1913
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
1824
1914
  )
1825
1915
  .select({
1826
1916
  totalActiveUsers: sql`COUNT(au.id)`,
1827
- totalCompletedOrders: sql`COUNT(co.id)`
1917
+ totalCompletedOrders: sql`COUNT(co.id)`,
1828
1918
  })
1829
1919
  .from(sql`activeUsers au`)
1830
1920
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -1833,21 +1923,23 @@ const userStats = await forgeSQL
1833
1923
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1834
1924
  async () => {
1835
1925
  const users = await forgeSQL.selectFrom(usersTable);
1836
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1926
+ const orders = await forgeSQL
1927
+ .selectFrom(ordersTable)
1928
+ .where(eq(ordersTable.userId, usersTable.id));
1837
1929
  return { users, orders };
1838
1930
  },
1839
1931
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1840
1932
  const threshold = 500; // ms baseline for this resolver
1841
-
1933
+
1842
1934
  if (totalDbExecutionTime > threshold * 1.5) {
1843
1935
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1844
1936
  await printQueriesWithPlan(); // Analyze and print query execution plans
1845
1937
  } else if (totalDbExecutionTime > threshold) {
1846
1938
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1847
1939
  }
1848
-
1940
+
1849
1941
  console.log(`DB response size: ${totalResponseSize} bytes`);
1850
- }
1942
+ },
1851
1943
  );
1852
1944
  ```
1853
1945
 
@@ -1883,9 +1975,9 @@ const options = {
1883
1975
  tableName: "users",
1884
1976
  versionField: {
1885
1977
  fieldName: "updatedAt",
1886
- }
1887
- }
1888
- }
1978
+ },
1979
+ },
1980
+ },
1889
1981
  };
1890
1982
 
1891
1983
  const forgeSQL = new ForgeSQL(options);
@@ -1896,24 +1988,26 @@ const forgeSQL = new ForgeSQL(options);
1896
1988
  ```typescript
1897
1989
  // The version field will be automatically handled
1898
1990
  await forgeSQL.modifyWithVersioning().updateById(
1899
- {
1900
- id: 1,
1991
+ {
1992
+ id: 1,
1901
1993
  name: "Updated Name",
1902
- updatedAt: new Date() // Will be automatically set if not provided
1903
- },
1904
- Users
1994
+ updatedAt: new Date(), // Will be automatically set if not provided
1995
+ },
1996
+ Users,
1905
1997
  );
1906
1998
  ```
1999
+
1907
2000
  or with cache support
2001
+
1908
2002
  ```typescript
1909
2003
  // The version field will be automatically handled
1910
2004
  await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
1911
- {
1912
- id: 1,
2005
+ {
2006
+ id: 1,
1913
2007
  name: "Updated Name",
1914
- updatedAt: new Date() // Will be automatically set if not provided
1915
- },
1916
- Users
2008
+ updatedAt: new Date(), // Will be automatically set if not provided
2009
+ },
2010
+ Users,
1917
2011
  );
1918
2012
  ```
1919
2013
 
@@ -1921,16 +2015,16 @@ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
1921
2015
 
1922
2016
  The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
1923
2017
 
1924
- | Option | Type | Description |
1925
- | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1926
- | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
1927
- | `logCache` | `boolean` | Enables logging of cache operations (hits, misses, evictions) in the Atlassian Forge Developer Console. Useful for debugging caching issues. Defaults to `false`. |
2018
+ | Option | Type | Description |
2019
+ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2020
+ | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
2021
+ | `logCache` | `boolean` | Enables logging of cache operations (hits, misses, evictions) in the Atlassian Forge Developer Console. Useful for debugging caching issues. Defaults to `false`. |
1928
2022
  | `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. |
1929
- | `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. |
1930
- | `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"`. |
1931
- | `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
1932
- | `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
1933
- | `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
2023
+ | `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. |
2024
+ | `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"`. |
2025
+ | `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes). |
2026
+ | `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`. |
2027
+ | `hints` | `object` | SQL hints for query optimization. Optional configuration for advanced query tuning. |
1934
2028
 
1935
2029
  ## CLI Commands
1936
2030
 
@@ -1949,23 +2043,39 @@ The CLI tool provides the following main commands:
1949
2043
 
1950
2044
  ### Installation
1951
2045
 
2046
+ The CLI tool must be installed as a local dependency and used via npm scripts in your `package.json`:
2047
+
2048
+ ```bash
2049
+ npm install forge-sql-orm-cli -D
2050
+ ```
2051
+
2052
+ ### Setup npm Scripts
2053
+
2054
+ Add the following scripts to your `package.json`:
2055
+
1952
2056
  ```bash
1953
- npm install -g forge-sql-orm-cli
2057
+ npm pkg set scripts.models:create="forge-sql-orm-cli generate:model --output src/entities --saveEnv"
2058
+ npm pkg set scripts.migration:create="forge-sql-orm-cli migrations:create --force --output src/migration --entitiesPath src/entities"
2059
+ npm pkg set scripts.migration:update="forge-sql-orm-cli migrations:update --entitiesPath src/entities --output src/migration"
1954
2060
  ```
1955
2061
 
1956
2062
  ### Basic Usage
1957
2063
 
2064
+ After setting up the scripts, use them via npm:
2065
+
1958
2066
  ```bash
1959
2067
  # Generate models from database
1960
- forge-sql-orm-cli generate:model --dbName myapp --output ./database/entities
2068
+ npm run models:create
1961
2069
 
1962
2070
  # Create migration
1963
- forge-sql-orm-cli migrations:create --dbName myapp --entitiesPath ./database/entities
2071
+ npm run migration:create
1964
2072
 
1965
2073
  # Update migration
1966
- forge-sql-orm-cli migrations:update --dbName myapp --entitiesPath ./database/entities
2074
+ npm run migration:update
1967
2075
  ```
1968
2076
 
2077
+ **Note:** The CLI tool is designed to work as a local dependency through npm scripts. Configuration is saved to `.env` file using the `--saveEnv` flag, so you only need to provide database credentials once.
2078
+
1969
2079
  For detailed information about all available options and advanced usage, see the [Full CLI Documentation](forge-sql-orm-cli/README.md).
1970
2080
 
1971
2081
  ## Web Triggers for Migrations
@@ -1975,7 +2085,8 @@ Forge-SQL-ORM provides web triggers for managing database migrations in Atlassia
1975
2085
  ### 1. Apply Migrations Trigger
1976
2086
 
1977
2087
  This trigger allows you to apply database migrations through a web endpoint. It's useful for:
1978
- - Manually triggering migrations
2088
+
2089
+ - Manually triggering migrations
1979
2090
  - Running migrations as part of your deployment process
1980
2091
  - Testing migrations in different environments
1981
2092
 
@@ -1990,22 +2101,23 @@ export const handlerMigration = async () => {
1990
2101
  ```
1991
2102
 
1992
2103
  Configure in `manifest.yml`:
2104
+
1993
2105
  ```yaml
1994
- webtrigger:
1995
- - key: invoke-schema-migration
1996
- function: runSchemaMigration
1997
- security:
1998
- egress:
1999
- allowDataEgress: false
2000
- allowedResponses:
2001
- - statusCode: 200
2002
- body: '{"body": "Migrations successfully executed"}'
2003
- sql:
2004
- - key: main
2005
- engine: mysql
2006
- function:
2007
- - key: runSchemaMigration
2008
- handler: index.handlerMigration
2106
+ webtrigger:
2107
+ - key: invoke-schema-migration
2108
+ function: runSchemaMigration
2109
+ security:
2110
+ egress:
2111
+ allowDataEgress: false
2112
+ allowedResponses:
2113
+ - statusCode: 200
2114
+ body: '{"body": "Migrations successfully executed"}'
2115
+ sql:
2116
+ - key: main
2117
+ engine: mysql
2118
+ function:
2119
+ - key: runSchemaMigration
2120
+ handler: index.handlerMigration
2009
2121
  ```
2010
2122
 
2011
2123
  ### 2. Drop Migrations Trigger
@@ -2013,11 +2125,12 @@ Configure in `manifest.yml`:
2013
2125
  ⚠️ **WARNING**: This trigger will permanently delete all data in the specified tables and clear the migrations history. This operation cannot be undone!
2014
2126
 
2015
2127
  This trigger allows you to completely reset your database schema. It's useful for:
2128
+
2016
2129
  - Development environments where you need to start fresh
2017
2130
  - Testing scenarios requiring a clean database
2018
2131
  - Resetting the database before applying new migrations
2019
2132
 
2020
- **Important**: The trigger will drop all tables including migration.
2133
+ **Important**: The trigger will drop all tables including migration.
2021
2134
 
2022
2135
  ```typescript
2023
2136
  // Example usage in your Forge app
@@ -2029,16 +2142,17 @@ export const dropMigrations = () => {
2029
2142
  ```
2030
2143
 
2031
2144
  Configure in `manifest.yml`:
2145
+
2032
2146
  ```yaml
2033
- webtrigger:
2034
- - key: drop-schema-migration
2035
- function: dropMigrations
2036
- sql:
2037
- - key: main
2038
- engine: mysql
2039
- function:
2040
- - key: dropMigrations
2041
- handler: index.dropMigrations
2147
+ webtrigger:
2148
+ - key: drop-schema-migration
2149
+ function: dropMigrations
2150
+ sql:
2151
+ - key: main
2152
+ engine: mysql
2153
+ function:
2154
+ - key: dropMigrations
2155
+ handler: index.dropMigrations
2042
2156
  ```
2043
2157
 
2044
2158
  ### 3. Fetch Schema Trigger
@@ -2046,12 +2160,14 @@ Configure in `manifest.yml`:
2046
2160
  ⚠️ **DEVELOPMENT ONLY**: This trigger is designed for development environments only and should not be used in production.
2047
2161
 
2048
2162
  This trigger retrieves the current database schema from Atlassian Forge SQL and generates SQL statements that can be used to recreate the database structure. It's useful for:
2163
+
2049
2164
  - Development environment setup
2050
2165
  - Schema documentation
2051
2166
  - Database structure verification
2052
2167
  - Creating backup scripts
2053
2168
 
2054
2169
  **Security Considerations**:
2170
+
2055
2171
  - This trigger exposes your database structure
2056
2172
  - It temporarily disables foreign key checks
2057
2173
  - It may expose sensitive table names and structures
@@ -2067,19 +2183,21 @@ export const fetchSchema = async () => {
2067
2183
  ```
2068
2184
 
2069
2185
  Configure in `manifest.yml`:
2186
+
2070
2187
  ```yaml
2071
- webtrigger:
2072
- - key: fetch-schema
2073
- function: fetchSchema
2074
- sql:
2075
- - key: main
2076
- engine: mysql
2077
- function:
2078
- - key: fetchSchema
2079
- handler: index.fetchSchema
2188
+ webtrigger:
2189
+ - key: fetch-schema
2190
+ function: fetchSchema
2191
+ sql:
2192
+ - key: main
2193
+ engine: mysql
2194
+ function:
2195
+ - key: fetchSchema
2196
+ handler: index.fetchSchema
2080
2197
  ```
2081
2198
 
2082
2199
  The response will contain SQL statements like:
2200
+
2083
2201
  ```sql
2084
2202
  SET foreign_key_checks = 0;
2085
2203
  CREATE TABLE IF NOT EXISTS users (...);
@@ -2090,6 +2208,7 @@ SET foreign_key_checks = 1;
2090
2208
  ### 4. Clear Cache Scheduler Trigger
2091
2209
 
2092
2210
  This trigger automatically cleans up expired cache entries based on their TTL (Time To Live). It's useful for:
2211
+
2093
2212
  - Automatic cache maintenance
2094
2213
  - Preventing cache storage from growing indefinitely
2095
2214
  - Ensuring optimal cache performance
@@ -2100,40 +2219,73 @@ This trigger automatically cleans up expired cache entries based on their TTL (T
2100
2219
  import { clearCacheSchedulerTrigger } from "forge-sql-orm";
2101
2220
 
2102
2221
  export const clearCache = () => {
2103
- return clearCacheSchedulerTrigger({
2222
+ return clearCacheSchedulerTrigger({
2104
2223
  cacheEntityName: "cache",
2105
2224
  });
2106
2225
  };
2107
2226
  ```
2108
2227
 
2109
2228
  Configure in `manifest.yml`:
2229
+
2110
2230
  ```yaml
2111
- scheduledTrigger:
2112
- - key: clear-cache-trigger
2113
- function: clearCache
2114
- interval: fiveMinute
2115
- function:
2116
- - key: clearCache
2117
- handler: index.clearCache
2231
+ scheduledTrigger:
2232
+ - key: clear-cache-trigger
2233
+ function: clearCache
2234
+ interval: fiveMinute
2235
+ function:
2236
+ - key: clearCache
2237
+ handler: index.clearCache
2118
2238
  ```
2119
2239
 
2120
2240
  **Available Intervals**:
2241
+
2121
2242
  - `fiveMinute` - Every 5 minutes
2122
2243
  - `hour` - Every hour
2123
2244
  - `day` - Every day
2124
2245
 
2246
+ ### 5. Slow Query Scheduler Trigger
2247
+
2248
+ This scheduler trigger automatically monitors and analyzes slow queries on a scheduled basis. For detailed information, see the [Slow Query Monitoring](#slow-query-monitoring) section.
2249
+
2250
+ **Quick Setup:**
2251
+
2252
+ ```typescript
2253
+ import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
2254
+
2255
+ const forgeSQL = new ForgeSQL();
2256
+
2257
+ export const slowQueryTrigger = () =>
2258
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 1, timeout: 3000 });
2259
+ ```
2260
+
2261
+ Configure in `manifest.yml`:
2262
+
2263
+ ```yaml
2264
+ scheduledTrigger:
2265
+ - key: slow-query-trigger
2266
+ function: slowQueryTrigger
2267
+ interval: hour
2268
+ function:
2269
+ - key: slowQueryTrigger
2270
+ handler: index.slowQueryTrigger
2271
+ ```
2272
+
2273
+ > **💡 Note**: For complete documentation, examples, and configuration options, see the [Slow Query Monitoring](#slow-query-monitoring) section.
2274
+
2125
2275
  ### Important Notes
2126
2276
 
2127
2277
  **Security Considerations**:
2128
- - The drop migrations trigger should be restricted to development environments
2129
- - The fetch schema trigger should only be used in development
2130
- - Consider implementing additional authentication for these endpoints
2278
+
2279
+ - The drop migrations trigger should be restricted to development environments
2280
+ - The fetch schema trigger should only be used in development
2281
+ - Consider implementing additional authentication for these endpoints
2131
2282
 
2132
2283
  **Best Practices**:
2133
- - Always backup your data before using the drop migrations trigger
2134
- - Test migrations in a development environment first
2135
- - Use these triggers as part of your deployment pipeline
2136
- - Monitor the execution logs in the Forge Developer Console
2284
+
2285
+ - Always backup your data before using the drop migrations trigger
2286
+ - Test migrations in a development environment first
2287
+ - Use these triggers as part of your deployment pipeline
2288
+ - Monitor the execution logs in the Forge Developer Console
2137
2289
 
2138
2290
  ## Query Analysis and Performance Optimization
2139
2291
 
@@ -2144,6 +2296,7 @@ Forge-SQL-ORM provides comprehensive query analysis tools to help you optimize y
2144
2296
  ### About Atlassian's Built-in Analysis Tools
2145
2297
 
2146
2298
  Atlassian provides comprehensive query analysis tools in the development console, including:
2299
+
2147
2300
  - Basic query performance metrics
2148
2301
  - Slow query tracking (queries over 500ms)
2149
2302
  - Basic execution statistics
@@ -2210,6 +2363,112 @@ The error analysis mechanism:
2210
2363
 
2211
2364
  > **💡 Tip**: The automatic error analysis only triggers for timeout and OOM errors. Other errors are logged normally without plan analysis.
2212
2365
 
2366
+ ### Slow Query Monitoring
2367
+
2368
+ Forge-SQL-ORM provides a scheduler trigger (`slowQuerySchedulerTrigger`) that automatically monitors and analyzes slow queries on an hourly basis. This trigger queries TiDB's slow query log system table and provides detailed performance information including SQL query text, memory usage, execution time, and execution plans.
2369
+
2370
+ #### Key Features
2371
+
2372
+ - **Automatic Monitoring**: Runs on a scheduled interval (recommended: hourly)
2373
+ - **Detailed Performance Metrics**: Memory usage, execution time, and execution plans
2374
+ - **Console Logging**: Results are automatically logged to the Forge Developer Console
2375
+ - **Configurable Time Window**: Analyze queries from the last N hours (default: 1 hour)
2376
+ - **Automatic Plan Retrieval**: Execution plans are included for all slow queries
2377
+
2378
+ #### Basic Setup
2379
+
2380
+ **1. Create the trigger function:**
2381
+
2382
+ ```typescript
2383
+ import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
2384
+
2385
+ const forgeSQL = new ForgeSQL();
2386
+
2387
+ // Monitor slow queries from the last hour (recommended for hourly schedule)
2388
+ export const slowQueryTrigger = () =>
2389
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 1, timeout: 3000 });
2390
+ ```
2391
+
2392
+ **2. Configure in `manifest.yml`:**
2393
+
2394
+ ```yaml
2395
+ modules:
2396
+ scheduledTrigger:
2397
+ - key: slow-query-trigger
2398
+ function: slowQueryTrigger
2399
+ interval: hour # Run every hour
2400
+
2401
+ function:
2402
+ - key: slowQueryTrigger
2403
+ handler: index.slowQueryTrigger
2404
+ ```
2405
+
2406
+ #### Configuration Options
2407
+
2408
+ | Option | Type | Default | Description |
2409
+ | --------- | -------- | ------- | ---------------------------------------------------------- |
2410
+ | `hours` | `number` | `1` | Number of hours to look back for slow queries |
2411
+ | `timeout` | `number` | `3000` | Timeout in milliseconds for the diagnostic query execution |
2412
+
2413
+ #### Example Console Output
2414
+
2415
+ When slow queries are detected, you'll see output like this in the Forge Developer Console:
2416
+
2417
+ ```
2418
+ Found SlowQuery SQL: SELECT * FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE u.active = ? | Memory: 8.50 MB | Time: 2500.00 ms
2419
+ Plan:
2420
+ id task estRows operator info actRows execution info memory disk
2421
+ Projection_7 root 1000.00 forge_38dd1c6156b94bb59c2c9a45582bbfc7.users.id, ... 1000 time:2.5s, loops:1 8.50 MB N/A
2422
+ └─IndexHashJoin_14 root 1000.00 inner join, ... 1000 time:2.2s, loops:1 7.98 MB N/A
2423
+
2424
+ Found SlowQuery SQL: SELECT * FROM products WHERE category = ? ORDER BY created_at DESC | Memory: 6.25 MB | Time: 1800.00 ms
2425
+ Plan:
2426
+ ...
2427
+ ```
2428
+
2429
+ #### Advanced Configuration
2430
+
2431
+ ```typescript
2432
+ import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
2433
+
2434
+ const forgeSQL = new ForgeSQL();
2435
+
2436
+ // Monitor queries from the last 6 hours (for less frequent checks)
2437
+ export const sixHourSlowQueryTrigger = () =>
2438
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 6, timeout: 5000 });
2439
+
2440
+ // Monitor queries from the last 24 hours (daily monitoring)
2441
+ export const dailySlowQueryTrigger = () =>
2442
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 24, timeout: 3000 });
2443
+ ```
2444
+
2445
+ #### How It Works
2446
+
2447
+ 1. **Scheduled Execution**: The trigger runs automatically on the configured interval (hourly recommended)
2448
+ 2. **Query Analysis**: Queries TiDB's slow query log system table for queries executed within the specified time window
2449
+ 3. **Performance Metrics**: Extracts and logs:
2450
+ - SQL query text (sanitized for readability)
2451
+ - Maximum memory usage (in MB)
2452
+ - Query execution time (in ms)
2453
+ - Detailed execution plan
2454
+ 4. **Console Logging**: Results are logged to the Forge Developer Console via `console.warn()` for easy monitoring
2455
+
2456
+ #### Best Practices
2457
+
2458
+ - **Hourly Intervals**: Use `interval: hour` for timely detection of slow queries
2459
+ - **Default Time Window**: 1 hour is recommended for hourly schedules to avoid overlap
2460
+ - **Monitor Regularly**: Check console logs regularly to identify patterns in slow queries
2461
+
2462
+ #### Benefits
2463
+
2464
+ - **Proactive Monitoring**: Catch slow queries before they become critical issues
2465
+ - **Performance Trends**: Track query performance over time
2466
+ - **Optimization Insights**: Execution plans help identify optimization opportunities
2467
+ - **Zero Manual Intervention**: Fully automated monitoring with scheduled execution
2468
+ - **Production Safe**: Works silently in the background, only logs when slow queries are found
2469
+
2470
+ > **💡 Tip**: The trigger queries up to 50 slow queries to prevent excessive logging. Transient timeouts are usually fine; repeated timeouts indicate the diagnostic query itself is slow and should be investigated.
2471
+
2213
2472
  ### Available Analysis Tools
2214
2473
 
2215
2474
  ```typescript
@@ -2230,59 +2489,57 @@ const analyzeForgeSql = forgeSQL.analyze();
2230
2489
 
2231
2490
  // Analyze a Drizzle query
2232
2491
  const plan = await analyzeForgeSql.explain(
2233
- forgeSQL.select({
2234
- table1: testEntityJoin1,
2235
- table2: { name: testEntityJoin2.name, email: testEntityJoin2.email },
2236
- count: rawSql<number>`COUNT(*)`,
2237
- table3: {
2238
- table12: testEntityJoin1.name,
2239
- table22: testEntityJoin2.email,
2240
- table32: testEntity.id
2241
- },
2242
- })
2243
- .from(testEntityJoin1)
2244
- .innerJoin(testEntityJoin2, eq(testEntityJoin1.id, testEntityJoin2.id))
2492
+ forgeSQL
2493
+ .select({
2494
+ table1: testEntityJoin1,
2495
+ table2: { name: testEntityJoin2.name, email: testEntityJoin2.email },
2496
+ count: rawSql<number>`COUNT(*)`,
2497
+ table3: {
2498
+ table12: testEntityJoin1.name,
2499
+ table22: testEntityJoin2.email,
2500
+ table32: testEntity.id,
2501
+ },
2502
+ })
2503
+ .from(testEntityJoin1)
2504
+ .innerJoin(testEntityJoin2, eq(testEntityJoin1.id, testEntityJoin2.id)),
2245
2505
  );
2246
2506
 
2247
2507
  // Analyze a raw SQL query
2248
- const rawPlan = await analyzeForgeSql.explainRaw(
2249
- "SELECT * FROM users WHERE id = ?",
2250
- [1]
2251
- );
2508
+ const rawPlan = await analyzeForgeSql.explainRaw("SELECT * FROM users WHERE id = ?", [1]);
2252
2509
 
2253
2510
  // Analyze new methods
2254
2511
  const usersFromPlan = await analyzeForgeSql.explain(
2255
- forgeSQL.selectFrom(users).where(eq(users.active, true))
2512
+ forgeSQL.selectFrom(users).where(eq(users.active, true)),
2256
2513
  );
2257
2514
 
2258
2515
  const usersCacheablePlan = await analyzeForgeSql.explain(
2259
- forgeSQL.selectCacheableFrom(users).where(eq(users.active, true))
2516
+ forgeSQL.selectCacheableFrom(users).where(eq(users.active, true)),
2260
2517
  );
2261
2518
 
2262
2519
  // Analyze Common Table Expressions (CTEs)
2263
2520
  const ctePlan = await analyzeForgeSql.explain(
2264
2521
  forgeSQL
2265
2522
  .with(
2266
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2267
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2523
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
2524
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
2268
2525
  )
2269
2526
  .select({
2270
2527
  totalActiveUsers: sql`COUNT(au.id)`,
2271
- totalCompletedOrders: sql`COUNT(co.id)`
2528
+ totalCompletedOrders: sql`COUNT(co.id)`,
2272
2529
  })
2273
2530
  .from(sql`activeUsers au`)
2274
- .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`))
2531
+ .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`)),
2275
2532
  );
2276
2533
  ```
2277
2534
 
2278
2535
  This analysis provides insights into:
2536
+
2279
2537
  - How the database executes your query
2280
2538
  - Which indexes are being used
2281
2539
  - Estimated vs actual row counts
2282
2540
  - Resource usage at each step
2283
2541
  - Performance optimization opportunities
2284
2542
 
2285
-
2286
2543
  ## Migration Guide
2287
2544
 
2288
2545
  ### Migrating from 2.0.x to 2.1.x
@@ -2292,18 +2549,20 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2292
2549
  #### 1. Method Renaming (BREAKING CHANGES)
2293
2550
 
2294
2551
  **Removed Methods:**
2552
+
2295
2553
  - `forgeSQL.modify()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2296
2554
  - `forgeSQL.crud()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
2297
2555
 
2298
2556
  **Migration Steps:**
2299
2557
 
2300
2558
  1. **Replace `modify()` calls:**
2559
+
2301
2560
  ```typescript
2302
2561
  // ❌ Old (2.0.x) - NO LONGER WORKS
2303
2562
  await forgeSQL.modify().insert(Users, [userData]);
2304
2563
  await forgeSQL.modify().updateById(updateData, Users);
2305
2564
  await forgeSQL.modify().deleteById(1, Users);
2306
-
2565
+
2307
2566
  // ✅ New (2.1.x) - REQUIRED
2308
2567
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2309
2568
  await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
@@ -2311,12 +2570,13 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2311
2570
  ```
2312
2571
 
2313
2572
  2. **Replace `crud()` calls:**
2573
+
2314
2574
  ```typescript
2315
2575
  // ❌ Old (2.0.x) - NO LONGER WORKS
2316
2576
  await forgeSQL.crud().insert(Users, [userData]);
2317
2577
  await forgeSQL.crud().updateById(updateData, Users);
2318
2578
  await forgeSQL.crud().deleteById(1, Users);
2319
-
2579
+
2320
2580
  // ✅ New (2.1.x) - REQUIRED
2321
2581
  await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
2322
2582
  await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
@@ -2326,8 +2586,9 @@ This section covers the breaking changes introduced in version 2.1.x and how to
2326
2586
  #### 2. New API Methods
2327
2587
 
2328
2588
  **New Methods Available:**
2589
+
2329
2590
  - `forgeSQL.insert()` - Basic Drizzle operations
2330
- - `forgeSQL.update()` - Basic Drizzle operations
2591
+ - `forgeSQL.update()` - Basic Drizzle operations
2331
2592
  - `forgeSQL.delete()` - Basic Drizzle operations
2332
2593
  - `forgeSQL.insertAndEvictCache()` - Basic Drizzle operations with evict cache after execution
2333
2594
  - `forgeSQL.updateAndEvictCache()` - Basic Drizzle operations with evict cache after execution
@@ -2355,46 +2616,43 @@ await forgeSQL.insert(Users).values(userData);
2355
2616
  await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
2356
2617
 
2357
2618
  // ✅ New query methods for better performance
2358
- const users = await forgeSQL.selectFrom(Users)
2359
- .where(eq(Users.active, true));
2619
+ const users = await forgeSQL.selectFrom(Users).where(eq(Users.active, true));
2360
2620
 
2361
- const usersDistinct = await forgeSQL.selectDistinctFrom(Users)
2362
- .where(eq(Users.active, true));
2621
+ const usersDistinct = await forgeSQL.selectDistinctFrom(Users).where(eq(Users.active, true));
2363
2622
 
2364
- const usersCacheable = await forgeSQL.selectCacheableFrom(Users)
2365
- .where(eq(Users.active, true));
2623
+ const usersCacheable = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
2366
2624
 
2367
2625
  // ✅ Raw SQL execution with caching
2368
- const rawUsers = await forgeSQL.execute(
2369
- "SELECT * FROM users WHERE active = ?",
2370
- [true]
2371
- );
2626
+ const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
2372
2627
 
2628
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
2373
2629
  const cachedRawUsers = await forgeSQL.executeCacheable(
2374
- "SELECT * FROM users WHERE active = ?",
2375
- [true],
2376
- 300
2630
+ "SELECT * FROM `users` WHERE active = ?",
2631
+ [true],
2632
+ 300,
2377
2633
  );
2378
2634
 
2379
2635
  // ✅ Raw SQL execution with metadata capture and performance monitoring
2380
2636
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
2381
2637
  async () => {
2382
2638
  const users = await forgeSQL.selectFrom(usersTable);
2383
- const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
2639
+ const orders = await forgeSQL
2640
+ .selectFrom(ordersTable)
2641
+ .where(eq(ordersTable.userId, usersTable.id));
2384
2642
  return { users, orders };
2385
2643
  },
2386
2644
  (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2387
2645
  const threshold = 500; // ms baseline for this resolver
2388
-
2646
+
2389
2647
  if (totalDbExecutionTime > threshold * 1.5) {
2390
2648
  console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
2391
2649
  await printQueriesWithPlan(); // Analyze and print query execution plans
2392
2650
  } else if (totalDbExecutionTime > threshold) {
2393
2651
  console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
2394
2652
  }
2395
-
2653
+
2396
2654
  console.log(`DB response size: ${totalResponseSize} bytes`);
2397
- }
2655
+ },
2398
2656
  );
2399
2657
 
2400
2658
  // ✅ DDL operations for schema modifications
@@ -2418,25 +2676,25 @@ await forgeSQL.executeDDLActions(async () => {
2418
2676
  SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
2419
2677
  WHERE AVG_LATENCY > 1000000
2420
2678
  `);
2421
-
2679
+
2422
2680
  // Execute complex analysis queries in DDL context
2423
2681
  const performanceData = await forgeSQL.execute(`
2424
2682
  SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
2425
2683
  WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
2426
2684
  `);
2427
-
2685
+
2428
2686
  return { slowQueries, performanceData };
2429
2687
  });
2430
2688
 
2431
2689
  // ✅ Common Table Expressions (CTEs)
2432
2690
  const userStats = await forgeSQL
2433
2691
  .with(
2434
- forgeSQL.selectFrom(users).where(eq(users.active, true)).as('activeUsers'),
2435
- forgeSQL.selectFrom(orders).where(eq(orders.status, 'completed')).as('completedOrders')
2692
+ forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
2693
+ forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
2436
2694
  )
2437
2695
  .select({
2438
2696
  totalActiveUsers: sql`COUNT(au.id)`,
2439
- totalCompletedOrders: sql`COUNT(co.id)`
2697
+ totalCompletedOrders: sql`COUNT(co.id)`,
2440
2698
  })
2441
2699
  .from(sql`activeUsers au`)
2442
2700
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
@@ -2450,7 +2708,7 @@ You can use a simple find-and-replace to migrate your code:
2450
2708
  # Replace modify() calls
2451
2709
  find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.modify()/forgeSQL.modifyWithVersioning()/g'
2452
2710
 
2453
- # Replace crud() calls
2711
+ # Replace crud() calls
2454
2712
  find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.modifyWithVersioning()/g'
2455
2713
  ```
2456
2714
 
@@ -2462,5 +2720,6 @@ find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.
2462
2720
  - ✅ **Migration Required**: You must update your code to use the new methods
2463
2721
 
2464
2722
  ## License
2723
+
2465
2724
  This project is licensed under the **MIT License**.
2466
2725
  Feel free to use it for commercial and personal projects.