forge-sql-orm 2.1.12 → 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.
- package/README.md +662 -548
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.js +257 -0
- package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -0
- package/dist/core/ForgeSQLCacheOperations.js +172 -0
- package/dist/core/ForgeSQLCacheOperations.js.map +1 -0
- package/dist/core/ForgeSQLCrudOperations.js +349 -0
- package/dist/core/ForgeSQLCrudOperations.js.map +1 -0
- package/dist/core/ForgeSQLORM.js +1191 -0
- package/dist/core/ForgeSQLORM.js.map +1 -0
- package/dist/core/ForgeSQLQueryBuilder.js +77 -0
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -0
- package/dist/core/ForgeSQLSelectOperations.js +81 -0
- package/dist/core/ForgeSQLSelectOperations.js.map +1 -0
- package/dist/core/SystemTables.js +258 -0
- package/dist/core/SystemTables.js.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +527 -0
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -0
- package/dist/utils/cacheContextUtils.d.ts.map +1 -1
- package/dist/utils/cacheContextUtils.js +198 -0
- package/dist/utils/cacheContextUtils.js.map +1 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +383 -0
- package/dist/utils/cacheUtils.js.map +1 -0
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +139 -0
- package/dist/utils/forgeDriver.js.map +1 -0
- package/dist/utils/forgeDriverProxy.js +68 -0
- package/dist/utils/forgeDriverProxy.js.map +1 -0
- package/dist/utils/metadataContextUtils.js +28 -0
- package/dist/utils/metadataContextUtils.js.map +1 -0
- package/dist/utils/requestTypeContextUtils.js +10 -0
- package/dist/utils/requestTypeContextUtils.js.map +1 -0
- package/dist/utils/sqlHints.js +52 -0
- package/dist/utils/sqlHints.js.map +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +590 -0
- package/dist/utils/sqlUtils.js.map +1 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.js +77 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.js.map +1 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.js +83 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -0
- package/dist/webtriggers/dropMigrationWebTrigger.js +54 -0
- package/dist/webtriggers/dropMigrationWebTrigger.js.map +1 -0
- package/dist/webtriggers/dropTablesMigrationWebTrigger.js +54 -0
- package/dist/webtriggers/dropTablesMigrationWebTrigger.js.map +1 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.js +82 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.js.map +1 -0
- package/dist/webtriggers/index.js +40 -0
- package/dist/webtriggers/index.js.map +1 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.js +80 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.js.map +1 -0
- package/package.json +28 -23
- package/src/core/ForgeSQLAnalyseOperations.ts +3 -2
- package/src/lib/drizzle/extensions/additionalActions.ts +11 -0
- package/src/utils/cacheContextUtils.ts +9 -6
- package/src/utils/cacheUtils.ts +6 -4
- package/src/utils/forgeDriver.ts +3 -7
- package/src/utils/sqlUtils.ts +33 -34
- package/dist/ForgeSQLORM.js +0 -3922
- package/dist/ForgeSQLORM.js.map +0 -1
- package/dist/ForgeSQLORM.mjs +0 -3905
- package/dist/ForgeSQLORM.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
[](https://coveralls.io/github/vzakharchenko/forge-sql-orm?branch=master)
|
|
12
12
|
[](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/) )
|
|
@@ -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,6 +66,7 @@
|
|
|
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
72
|
- [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
|
|
@@ -69,12 +74,14 @@
|
|
|
69
74
|
- [Date and Time Types](#date-and-time-types)
|
|
70
75
|
|
|
71
76
|
### 🛠️ Development Tools
|
|
77
|
+
|
|
72
78
|
- [CLI Commands](#cli-commands) | [CLI Documentation](forge-sql-orm-cli/README.md)
|
|
73
79
|
- [Web Triggers for Migrations](#web-triggers-for-migrations)
|
|
74
80
|
- [Step-by-Step Migration Workflow](#step-by-step-migration-workflow)
|
|
75
81
|
- [Drop Migrations](#drop-migrations)
|
|
76
82
|
|
|
77
83
|
### 📚 Examples
|
|
84
|
+
|
|
78
85
|
- [Simple Example](examples/forge-sql-orm-example-simple)
|
|
79
86
|
- [Drizzle Driver Example](examples/forge-sql-orm-example-drizzle-driver-simple)
|
|
80
87
|
- [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking)
|
|
@@ -85,17 +92,20 @@
|
|
|
85
92
|
- [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
|
|
86
93
|
|
|
87
94
|
### 📚 Reference
|
|
95
|
+
|
|
88
96
|
- [ForgeSqlOrmOptions](#forgesqlormoptions)
|
|
89
97
|
- [Migration Guide](#migration-guide)
|
|
90
98
|
|
|
91
99
|
## 🚀 Quick Navigation
|
|
92
100
|
|
|
93
101
|
**New to Forge-SQL-ORM?** Start here:
|
|
102
|
+
|
|
94
103
|
- [Quick Start](#quick-start) - Get up and running in 5 minutes
|
|
95
104
|
- [Installation](#installation) - Complete setup guide
|
|
96
105
|
- [Basic Usage Examples](#fetch-data) - Simple query examples
|
|
97
106
|
|
|
98
107
|
**Looking for specific features?**
|
|
108
|
+
|
|
99
109
|
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
|
|
100
110
|
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
|
|
101
111
|
- [Optimistic Locking](#optimistic-locking) - Data consistency
|
|
@@ -103,6 +113,7 @@
|
|
|
103
113
|
- [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
|
|
104
114
|
|
|
105
115
|
**Looking for practical examples?**
|
|
116
|
+
|
|
106
117
|
- [Simple Example](examples/forge-sql-orm-example-simple) - Basic ORM usage
|
|
107
118
|
- [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
|
|
108
119
|
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
|
|
@@ -111,24 +122,27 @@
|
|
|
111
122
|
|
|
112
123
|
## Usage Approaches
|
|
113
124
|
|
|
114
|
-
|
|
115
125
|
### 1. Full Forge-SQL-ORM Usage
|
|
126
|
+
|
|
116
127
|
```typescript
|
|
117
128
|
import ForgeSQL from "forge-sql-orm";
|
|
118
129
|
const forgeSQL = new ForgeSQL();
|
|
119
130
|
```
|
|
131
|
+
|
|
120
132
|
Best for: Advanced features like optimistic locking, automatic versioning, and automatic field name collision prevention in complex queries.
|
|
121
133
|
|
|
122
134
|
### 2. Direct Drizzle Usage
|
|
135
|
+
|
|
123
136
|
```typescript
|
|
124
137
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
125
138
|
import { forgeDriver } from "forge-sql-orm";
|
|
126
139
|
const db = drizzle(forgeDriver);
|
|
127
140
|
```
|
|
128
|
-
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
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.
|
|
130
143
|
|
|
131
144
|
### 3. Local Cache Optimization
|
|
145
|
+
|
|
132
146
|
```typescript
|
|
133
147
|
import ForgeSQL from "forge-sql-orm";
|
|
134
148
|
const forgeSQL = new ForgeSQL();
|
|
@@ -136,28 +150,28 @@ const forgeSQL = new ForgeSQL();
|
|
|
136
150
|
// Optimize repeated queries within a single invocation
|
|
137
151
|
await forgeSQL.executeWithLocalContext(async () => {
|
|
138
152
|
// Multiple queries here will benefit from local caching
|
|
139
|
-
const users = await forgeSQL
|
|
140
|
-
.
|
|
141
|
-
|
|
153
|
+
const users = await forgeSQL
|
|
154
|
+
.select({ id: users.id, name: users.name })
|
|
155
|
+
.from(users)
|
|
156
|
+
.where(eq(users.active, true));
|
|
157
|
+
|
|
142
158
|
// This query will use local cache (no database call)
|
|
143
|
-
const cachedUsers = await forgeSQL
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
// Using new methods for better performance
|
|
147
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
159
|
+
const cachedUsers = await forgeSQL
|
|
160
|
+
.select({ id: users.id, name: users.name })
|
|
161
|
+
.from(users)
|
|
148
162
|
.where(eq(users.active, true));
|
|
149
|
-
|
|
163
|
+
|
|
164
|
+
// Using new methods for better performance
|
|
165
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
166
|
+
|
|
150
167
|
// This will use local cache (no database call)
|
|
151
|
-
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
169
|
+
|
|
154
170
|
// Raw SQL with local caching
|
|
155
|
-
const rawUsers = await forgeSQL.execute(
|
|
156
|
-
"SELECT id, name FROM users WHERE active = ?",
|
|
157
|
-
[true]
|
|
158
|
-
);
|
|
171
|
+
const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
|
|
159
172
|
});
|
|
160
173
|
```
|
|
174
|
+
|
|
161
175
|
Best for: Performance optimization of repeated queries within resolvers or single invocation contexts.
|
|
162
176
|
|
|
163
177
|
## Field Name Collision Prevention in Complex Queries
|
|
@@ -167,6 +181,7 @@ When working with complex queries involving multiple tables (joins, inner joins,
|
|
|
167
181
|
Forge-SQL-ORM provides two ways to handle this:
|
|
168
182
|
|
|
169
183
|
### Using Forge-SQL-ORM
|
|
184
|
+
|
|
170
185
|
```typescript
|
|
171
186
|
import ForgeSQL from "forge-sql-orm";
|
|
172
187
|
|
|
@@ -174,12 +189,13 @@ const forgeSQL = new ForgeSQL();
|
|
|
174
189
|
|
|
175
190
|
// Automatic field name collision prevention
|
|
176
191
|
await forgeSQL
|
|
177
|
-
.select({user: users, order: orders})
|
|
192
|
+
.select({ user: users, order: orders })
|
|
178
193
|
.from(orders)
|
|
179
194
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
180
195
|
```
|
|
181
196
|
|
|
182
197
|
### Using Direct Drizzle
|
|
198
|
+
|
|
183
199
|
```typescript
|
|
184
200
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
185
201
|
import { forgeDriver, patchDbWithSelectAliased } from "forge-sql-orm";
|
|
@@ -188,18 +204,18 @@ const db = patchDbWithSelectAliased(drizzle(forgeDriver));
|
|
|
188
204
|
|
|
189
205
|
// Manual field name collision prevention
|
|
190
206
|
await db
|
|
191
|
-
.selectAliased({user: users, order: orders})
|
|
207
|
+
.selectAliased({ user: users, order: orders })
|
|
192
208
|
.from(orders)
|
|
193
209
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
194
210
|
```
|
|
195
211
|
|
|
196
212
|
### Important Notes
|
|
213
|
+
|
|
197
214
|
- This is a specific behavior of Atlassian Forge SQL, not Drizzle ORM
|
|
198
215
|
- For complex queries involving multiple tables, it's recommended to always specify select fields and avoid using `select()` without field selection
|
|
199
216
|
- The solution automatically creates unique aliases for each field by prefixing them with the table name
|
|
200
217
|
- This ensures that fields with the same name from different tables remain distinct in the query results
|
|
201
218
|
|
|
202
|
-
|
|
203
219
|
## Installation
|
|
204
220
|
|
|
205
221
|
Forge-SQL-ORM is designed to work with @forge/sql and requires some additional setup to ensure compatibility within Atlassian Forge.
|
|
@@ -207,16 +223,35 @@ Forge-SQL-ORM is designed to work with @forge/sql and requires some additional s
|
|
|
207
223
|
✅ Step 1: Install Dependencies
|
|
208
224
|
|
|
209
225
|
**Basic installation (without caching):**
|
|
226
|
+
|
|
210
227
|
```sh
|
|
211
228
|
npm install forge-sql-orm @forge/sql drizzle-orm -S
|
|
212
229
|
```
|
|
213
230
|
|
|
214
231
|
**With caching support:**
|
|
232
|
+
|
|
215
233
|
```sh
|
|
216
234
|
npm install forge-sql-orm @forge/sql @forge/kvs drizzle-orm -S
|
|
217
235
|
```
|
|
218
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
|
+
|
|
219
253
|
This will:
|
|
254
|
+
|
|
220
255
|
- Install Forge-SQL-ORM (the ORM for @forge/sql)
|
|
221
256
|
- Install @forge/sql, the Forge database layer
|
|
222
257
|
- Install @forge/kvs, the Forge Key-Value Store for caching (optional, only needed for caching features)
|
|
@@ -227,6 +262,7 @@ This will:
|
|
|
227
262
|
## Quick Start
|
|
228
263
|
|
|
229
264
|
### 1. Basic Setup
|
|
265
|
+
|
|
230
266
|
```typescript
|
|
231
267
|
import ForgeSQL from "forge-sql-orm";
|
|
232
268
|
|
|
@@ -238,44 +274,49 @@ const users = await forgeSQL.select().from(users);
|
|
|
238
274
|
```
|
|
239
275
|
|
|
240
276
|
### 2. With Caching (Optional)
|
|
277
|
+
|
|
241
278
|
```typescript
|
|
242
279
|
import ForgeSQL from "forge-sql-orm";
|
|
243
280
|
|
|
244
281
|
// Initialize with caching
|
|
245
282
|
const forgeSQL = new ForgeSQL({
|
|
246
283
|
cacheEntityName: "cache",
|
|
247
|
-
cacheTTL: 300
|
|
284
|
+
cacheTTL: 300,
|
|
248
285
|
});
|
|
249
286
|
|
|
250
287
|
// Cached query
|
|
251
|
-
const users = await forgeSQL
|
|
252
|
-
.
|
|
288
|
+
const users = await forgeSQL
|
|
289
|
+
.selectCacheable({ id: users.id, name: users.name })
|
|
290
|
+
.from(users)
|
|
291
|
+
.where(eq(users.active, true));
|
|
253
292
|
```
|
|
254
293
|
|
|
255
294
|
### 3. Local Cache Optimization
|
|
295
|
+
|
|
256
296
|
```typescript
|
|
257
297
|
// Optimize repeated queries within a single invocation
|
|
258
298
|
await forgeSQL.executeWithLocalContext(async () => {
|
|
259
|
-
const users = await forgeSQL
|
|
260
|
-
.
|
|
261
|
-
|
|
299
|
+
const users = await forgeSQL
|
|
300
|
+
.select({ id: users.id, name: users.name })
|
|
301
|
+
.from(users)
|
|
302
|
+
.where(eq(users.active, true));
|
|
303
|
+
|
|
262
304
|
// This query will use local cache (no database call)
|
|
263
|
-
const cachedUsers = await forgeSQL
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
// Using new methods for better performance
|
|
267
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
305
|
+
const cachedUsers = await forgeSQL
|
|
306
|
+
.select({ id: users.id, name: users.name })
|
|
307
|
+
.from(users)
|
|
268
308
|
.where(eq(users.active, true));
|
|
269
|
-
|
|
309
|
+
|
|
310
|
+
// Using new methods for better performance
|
|
311
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
312
|
+
|
|
270
313
|
// Raw SQL with local caching
|
|
271
|
-
const rawUsers = await forgeSQL.execute(
|
|
272
|
-
"SELECT id, name FROM users WHERE active = ?",
|
|
273
|
-
[true]
|
|
274
|
-
);
|
|
314
|
+
const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
|
|
275
315
|
});
|
|
276
316
|
```
|
|
277
317
|
|
|
278
318
|
### 4. Resolver Performance Monitoring
|
|
319
|
+
|
|
279
320
|
```typescript
|
|
280
321
|
// Resolver with performance monitoring
|
|
281
322
|
resolver.define("fetch", async (req: Request) => {
|
|
@@ -284,20 +325,23 @@ resolver.define("fetch", async (req: Request) => {
|
|
|
284
325
|
async () => {
|
|
285
326
|
// Resolver logic with multiple queries
|
|
286
327
|
const users = await forgeSQL.selectFrom(demoUsers);
|
|
287
|
-
const orders = await forgeSQL
|
|
328
|
+
const orders = await forgeSQL
|
|
329
|
+
.selectFrom(demoOrders)
|
|
288
330
|
.where(eq(demoOrders.userId, demoUsers.id));
|
|
289
331
|
return { users, orders };
|
|
290
332
|
},
|
|
291
333
|
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
292
334
|
const threshold = 500; // ms baseline for this resolver
|
|
293
|
-
|
|
335
|
+
|
|
294
336
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
295
|
-
console.warn(
|
|
337
|
+
console.warn(
|
|
338
|
+
`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
|
|
339
|
+
);
|
|
296
340
|
await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
|
|
297
341
|
} else if (totalDbExecutionTime > threshold) {
|
|
298
342
|
console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
|
|
299
343
|
}
|
|
300
|
-
}
|
|
344
|
+
},
|
|
301
345
|
);
|
|
302
346
|
} catch (e) {
|
|
303
347
|
const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
|
|
@@ -308,6 +352,7 @@ resolver.define("fetch", async (req: Request) => {
|
|
|
308
352
|
```
|
|
309
353
|
|
|
310
354
|
### 5. Next Steps
|
|
355
|
+
|
|
311
356
|
- [Full Installation Guide](#installation) - Complete setup instructions
|
|
312
357
|
- [Core Features](#core-features) - Learn about key capabilities
|
|
313
358
|
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
|
|
@@ -343,48 +388,44 @@ const db = forgeSQL.getDrizzleQueryBuilder();
|
|
|
343
388
|
const users = await db.select().from(users);
|
|
344
389
|
|
|
345
390
|
// Using new methods for enhanced functionality
|
|
346
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
347
|
-
.where(eq(users.active, true));
|
|
391
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
348
392
|
|
|
349
|
-
const usersDistinct = await forgeSQL.selectDistinctFrom(users)
|
|
350
|
-
.where(eq(users.active, true));
|
|
393
|
+
const usersDistinct = await forgeSQL.selectDistinctFrom(users).where(eq(users.active, true));
|
|
351
394
|
|
|
352
|
-
const usersCacheable = await forgeSQL.selectCacheableFrom(users)
|
|
353
|
-
.where(eq(users.active, true));
|
|
395
|
+
const usersCacheable = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
|
|
354
396
|
|
|
355
397
|
// Raw SQL execution
|
|
356
|
-
const rawUsers = await forgeSQL.execute(
|
|
357
|
-
"SELECT * FROM users WHERE active = ?",
|
|
358
|
-
[true]
|
|
359
|
-
);
|
|
398
|
+
const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
|
|
360
399
|
|
|
361
400
|
// Raw SQL with caching
|
|
362
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
|
|
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(
|
|
420
|
-
forgeSQL.selectFrom(orders).where(eq(orders.status,
|
|
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,15 +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
|
|
484
525
|
// ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
|
|
485
526
|
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
486
|
-
"SELECT * FROM `users` WHERE active = ?",
|
|
487
|
-
[true],
|
|
527
|
+
"SELECT * FROM `users` WHERE active = ?",
|
|
528
|
+
[true],
|
|
488
529
|
300
|
|
489
530
|
);
|
|
490
531
|
|
|
@@ -497,14 +538,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
497
538
|
},
|
|
498
539
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
499
540
|
const threshold = 500; // ms baseline for this resolver
|
|
500
|
-
|
|
541
|
+
|
|
501
542
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
502
543
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
503
544
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
504
545
|
} else if (totalDbExecutionTime > threshold) {
|
|
505
546
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
506
547
|
}
|
|
507
|
-
|
|
548
|
+
|
|
508
549
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
509
550
|
}
|
|
510
551
|
);
|
|
@@ -519,6 +560,7 @@ The caching system is optional and only needed if you want to use cache-related
|
|
|
519
560
|
To use caching, you need to use Forge-SQL-ORM methods that support cache management:
|
|
520
561
|
|
|
521
562
|
**Methods that perform cache eviction after execution and in cache context (batch eviction):**
|
|
563
|
+
|
|
522
564
|
- `forgeSQL.insertAndEvictCache()`
|
|
523
565
|
- `forgeSQL.updateAndEvictCache()`
|
|
524
566
|
- `forgeSQL.deleteAndEvictCache()`
|
|
@@ -528,6 +570,7 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
|
|
|
528
570
|
- `forgeSQL.getDrizzleQueryBuilder().deleteAndEvictCache()`
|
|
529
571
|
|
|
530
572
|
**Methods that participate in cache context only (batch eviction):**
|
|
573
|
+
|
|
531
574
|
- All methods except the default Drizzle methods:
|
|
532
575
|
- `forgeSQL.insert()`
|
|
533
576
|
- `forgeSQL.update()`
|
|
@@ -538,17 +581,20 @@ To use caching, you need to use Forge-SQL-ORM methods that support cache managem
|
|
|
538
581
|
- `forgeSQL.getDrizzleQueryBuilder().deleteWithCacheContext()`
|
|
539
582
|
|
|
540
583
|
**Methods do not do evict cache, better do not use with cache feature:**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
584
|
+
|
|
585
|
+
- `forgeSQL.getDrizzleQueryBuilder().insert()`
|
|
586
|
+
- `forgeSQL.getDrizzleQueryBuilder().update()`
|
|
587
|
+
- `forgeSQL.getDrizzleQueryBuilder().delete()`
|
|
544
588
|
|
|
545
589
|
**Cacheable methods:**
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
590
|
+
|
|
591
|
+
- `forgeSQL.selectCacheable()`
|
|
592
|
+
- `forgeSQL.selectDistinctCacheable()`
|
|
593
|
+
- `forgeSQL.getDrizzleQueryBuilder().selectAliasedCacheable()`
|
|
594
|
+
- `forgeSQL.getDrizzleQueryBuilder().selectAliasedDistinctCacheable()`
|
|
550
595
|
|
|
551
596
|
**Cache context example:**
|
|
597
|
+
|
|
552
598
|
```typescript
|
|
553
599
|
await forgeSQL.executeWithCacheContext(async () => {
|
|
554
600
|
// These methods participate in batch cache clearing
|
|
@@ -559,7 +605,6 @@ await forgeSQL.executeWithCacheContext(async () => {
|
|
|
559
605
|
});
|
|
560
606
|
```
|
|
561
607
|
|
|
562
|
-
|
|
563
608
|
The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
|
|
564
609
|
|
|
565
610
|
1. Resolver calls forge-sql-orm with a SQL query and parameters.
|
|
@@ -571,7 +616,6 @@ The diagram below shows the lifecycle of a cacheable query in Forge-SQL-ORM:
|
|
|
571
616
|
|
|
572
617
|

|
|
573
618
|
|
|
574
|
-
|
|
575
619
|
The diagram below shows how Evict Cache works in Forge-SQL-ORM:
|
|
576
620
|
|
|
577
621
|
1. **Data modification** is executed through `@forge/sql` (e.g., `UPDATE users ...`).
|
|
@@ -605,13 +649,13 @@ The diagram below shows how Cache Context works:
|
|
|
605
649
|
|
|
606
650
|

|
|
607
651
|
|
|
608
|
-
|
|
609
652
|
### Important Considerations
|
|
610
653
|
|
|
611
654
|
**@forge/kvs Limits:**
|
|
612
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.
|
|
613
656
|
|
|
614
657
|
**Caching Guidelines:**
|
|
658
|
+
|
|
615
659
|
- Don't cache everything - be selective about what to cache
|
|
616
660
|
- Don't cache simple and fast queries - sometimes direct query is faster than cache
|
|
617
661
|
- Consider data size and frequency of changes
|
|
@@ -619,7 +663,8 @@ Please review the [official @forge/kvs quotas and limits](https://developer.atla
|
|
|
619
663
|
- Use appropriate TTL values
|
|
620
664
|
|
|
621
665
|
**⚠️ Important Cache Limitations:**
|
|
622
|
-
|
|
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.
|
|
623
668
|
|
|
624
669
|
### Step 1: Install Dependencies
|
|
625
670
|
|
|
@@ -657,18 +702,18 @@ modules:
|
|
|
657
702
|
- key: clearCache
|
|
658
703
|
handler: index.clearCache
|
|
659
704
|
```
|
|
705
|
+
|
|
660
706
|
```typescript
|
|
661
707
|
// Example usage in your Forge app
|
|
662
708
|
import { clearCacheSchedulerTrigger } from "forge-sql-orm";
|
|
663
709
|
|
|
664
710
|
export const clearCache = () => {
|
|
665
|
-
return clearCacheSchedulerTrigger({
|
|
711
|
+
return clearCacheSchedulerTrigger({
|
|
666
712
|
cacheEntityName: "cache",
|
|
667
713
|
});
|
|
668
714
|
};
|
|
669
715
|
```
|
|
670
716
|
|
|
671
|
-
|
|
672
717
|
### Step 3: Configure ORM Options
|
|
673
718
|
|
|
674
719
|
Set the cache entity name in your ForgeSQL configuration:
|
|
@@ -685,6 +730,7 @@ const forgeSQL = new ForgeSQL(options);
|
|
|
685
730
|
```
|
|
686
731
|
|
|
687
732
|
**Important Notes:**
|
|
733
|
+
|
|
688
734
|
- The `cacheEntityName` must exactly match the `name` in your manifest storage entities
|
|
689
735
|
- The entity attributes (`sql`, `expiration`, `data`) are required for proper cache functionality
|
|
690
736
|
- Indexes on `sql` and `expiration` improve cache lookup performance
|
|
@@ -696,11 +742,14 @@ const forgeSQL = new ForgeSQL(options);
|
|
|
696
742
|
**Basic setup (without caching):**
|
|
697
743
|
|
|
698
744
|
**package.json:**
|
|
745
|
+
|
|
699
746
|
```shell
|
|
700
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
|
|
701
749
|
```
|
|
702
750
|
|
|
703
751
|
**manifest.yml:**
|
|
752
|
+
|
|
704
753
|
```yaml
|
|
705
754
|
modules:
|
|
706
755
|
sql:
|
|
@@ -709,6 +758,7 @@ modules:
|
|
|
709
758
|
```
|
|
710
759
|
|
|
711
760
|
**index.ts:**
|
|
761
|
+
|
|
712
762
|
```typescript
|
|
713
763
|
import ForgeSQL from "forge-sql-orm";
|
|
714
764
|
|
|
@@ -718,17 +768,18 @@ const forgeSQL = new ForgeSQL();
|
|
|
718
768
|
await forgeSQL.insert(Users, [userData]);
|
|
719
769
|
// Use versioned operations without caching
|
|
720
770
|
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
721
|
-
const users = await forgeSQL.select({id: Users.id});
|
|
722
|
-
|
|
723
|
-
|
|
771
|
+
const users = await forgeSQL.select({ id: Users.id });
|
|
724
772
|
```
|
|
725
773
|
|
|
726
774
|
**With caching support:**
|
|
775
|
+
|
|
727
776
|
```shell
|
|
728
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
|
|
729
779
|
```
|
|
730
780
|
|
|
731
781
|
**manifest.yml:**
|
|
782
|
+
|
|
732
783
|
```yaml
|
|
733
784
|
modules:
|
|
734
785
|
scheduledTrigger:
|
|
@@ -762,21 +813,23 @@ modules:
|
|
|
762
813
|
import ForgeSQL from "forge-sql-orm";
|
|
763
814
|
|
|
764
815
|
const forgeSQL = new ForgeSQL({
|
|
765
|
-
|
|
816
|
+
cacheEntityName: "cache",
|
|
766
817
|
});
|
|
767
818
|
|
|
768
|
-
import {clearCacheSchedulerTrigger} from "forge-sql-orm";
|
|
769
|
-
import {getTableColumns} from "drizzle-orm";
|
|
819
|
+
import { clearCacheSchedulerTrigger } from "forge-sql-orm";
|
|
820
|
+
import { getTableColumns } from "drizzle-orm";
|
|
770
821
|
|
|
771
822
|
export const clearCache = () => {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
823
|
+
return clearCacheSchedulerTrigger({
|
|
824
|
+
cacheEntityName: "cache",
|
|
825
|
+
});
|
|
775
826
|
};
|
|
776
827
|
|
|
777
|
-
|
|
778
828
|
// Now you can use caching features
|
|
779
|
-
const usersData = await forgeSQL
|
|
829
|
+
const usersData = await forgeSQL
|
|
830
|
+
.selectCacheable(getTableColumns(users))
|
|
831
|
+
.from(users)
|
|
832
|
+
.where(eq(users.active, true));
|
|
780
833
|
|
|
781
834
|
// simple insert
|
|
782
835
|
await forgeSQL.insertAndEvictCache(users, [userData]);
|
|
@@ -785,159 +838,194 @@ await forgeSQL.modifyWithVersioningAndEvictCache().insert(users, [userData]);
|
|
|
785
838
|
|
|
786
839
|
// use Cache Context
|
|
787
840
|
const data = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
+
});
|
|
801
868
|
// execute query and put result to kvs cache
|
|
802
|
-
await forgeSQL
|
|
803
|
-
|
|
804
|
-
|
|
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));
|
|
805
879
|
|
|
806
880
|
// get result from @foge/kvs cache without real @forge/sql call
|
|
807
|
-
await forgeSQL
|
|
808
|
-
|
|
809
|
-
|
|
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));
|
|
810
891
|
|
|
811
892
|
// use Local Cache for performance optimization
|
|
812
893
|
const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const cachedUsers = await forgeSQL.select({id: users.id, name: users.name})
|
|
819
|
-
.from(users).where(eq(users.active, true));
|
|
820
|
-
|
|
821
|
-
// Using new methods for better performance
|
|
822
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
823
|
-
.where(eq(users.active, true));
|
|
824
|
-
|
|
825
|
-
// This will use local cache (no database call)
|
|
826
|
-
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
827
|
-
.where(eq(users.active, true));
|
|
828
|
-
|
|
829
|
-
// Raw SQL with local caching
|
|
830
|
-
const rawUsers = await forgeSQL.execute(
|
|
831
|
-
"SELECT id, name FROM users WHERE active = ?",
|
|
832
|
-
[true]
|
|
833
|
-
);
|
|
834
|
-
|
|
835
|
-
// Insert operation - evicts local cache
|
|
836
|
-
await forgeSQL.insert(users).values({name: 'New User', active: true});
|
|
837
|
-
|
|
838
|
-
// Third query - hits database again and caches new result
|
|
839
|
-
const updatedUsers = await forgeSQL.select({id: users.id, name: users.name})
|
|
840
|
-
.from(users).where(eq(users.active, true));
|
|
841
|
-
|
|
842
|
-
return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
|
|
843
|
-
});
|
|
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));
|
|
844
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));
|
|
923
|
+
|
|
924
|
+
return { users, cachedUsers, updatedUsers, usersFrom, cachedUsersFrom, rawUsers };
|
|
925
|
+
});
|
|
845
926
|
```
|
|
846
927
|
|
|
847
928
|
## Choosing the Right Method - ForgeSQL ORM
|
|
848
929
|
|
|
849
930
|
### When to Use Each Approach
|
|
850
931
|
|
|
851
|
-
| Method
|
|
852
|
-
|
|
853
|
-
| `modifyWithVersioningAndEvictCache()` | High-concurrency scenarios with Cache support| ✅ Yes
|
|
854
|
-
| `modifyWithVersioning()`
|
|
855
|
-
| `insertAndEvictCache()`
|
|
856
|
-
| `updateAndEvictCache()`
|
|
857
|
-
| `deleteAndEvictCache()`
|
|
858
|
-
| `insert/update/delete`
|
|
859
|
-
| `selectFrom()`
|
|
860
|
-
| `selectDistinctFrom()`
|
|
861
|
-
| `selectCacheableFrom()`
|
|
862
|
-
| `selectDistinctCacheableFrom()`
|
|
863
|
-
| `execute()`
|
|
864
|
-
| `executeCacheable()`
|
|
865
|
-
| `executeDDL()`
|
|
866
|
-
| `executeDDLActions()`
|
|
867
|
-
| `with()`
|
|
868
|
-
|
|
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 |
|
|
869
949
|
|
|
870
950
|
## Choosing the Right Method - Direct Drizzle
|
|
871
951
|
|
|
872
952
|
### When to Use Each Approach
|
|
873
953
|
|
|
874
|
-
| Method
|
|
875
|
-
|
|
876
|
-
| `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations
|
|
877
|
-
| `insertAndEvictCache()`
|
|
878
|
-
| `updateAndEvictCache()`
|
|
879
|
-
| `deleteAndEvictCache()`
|
|
880
|
-
| `insert/update/delete`
|
|
881
|
-
| `selectFrom()`
|
|
882
|
-
| `selectDistinctFrom()`
|
|
883
|
-
| `selectCacheableFrom()`
|
|
884
|
-
| `selectDistinctCacheableFrom()`
|
|
885
|
-
| `execute()`
|
|
886
|
-
| `executeCacheable()`
|
|
887
|
-
| `executeWithMetadata()`
|
|
888
|
-
| `executeDDL()`
|
|
889
|
-
| `executeDDLActions()`
|
|
890
|
-
| `with()`
|
|
891
|
-
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 |
|
|
892
971
|
|
|
972
|
+
where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
|
|
893
973
|
|
|
894
974
|
## Step-by-Step Migration Workflow
|
|
895
975
|
|
|
896
|
-
1. **
|
|
976
|
+
1. **Install CLI and setup scripts**
|
|
897
977
|
|
|
898
|
-
```
|
|
899
|
-
|
|
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"
|
|
900
983
|
```
|
|
901
984
|
|
|
902
985
|
_(This is done only once when setting up the project)_
|
|
903
986
|
|
|
904
|
-
2. **
|
|
987
|
+
2. **Generate initial schema from an existing database**
|
|
905
988
|
|
|
906
989
|
```sh
|
|
907
|
-
|
|
990
|
+
npm run models:create
|
|
908
991
|
```
|
|
909
992
|
|
|
910
|
-
_(This
|
|
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
|
+
```
|
|
911
1000
|
|
|
912
|
-
|
|
1001
|
+
_(This initializes the database migration structure, also done once)_
|
|
913
1002
|
|
|
1003
|
+
4. **Deploy to Forge and verify that migrations work**
|
|
914
1004
|
- Deploy your **Forge app** with migrations.
|
|
915
1005
|
- Run migrations using a **Forge web trigger** or **Forge scheduler**.
|
|
916
1006
|
|
|
917
|
-
|
|
918
|
-
|
|
1007
|
+
5. **Modify the database (e.g., add a new column, index, etc.)**
|
|
919
1008
|
- Use **DbSchema** or manually alter the database schema.
|
|
920
1009
|
|
|
921
|
-
|
|
1010
|
+
6. **Update the migration**
|
|
922
1011
|
|
|
923
1012
|
```sh
|
|
924
|
-
|
|
1013
|
+
npm run migration:update
|
|
925
1014
|
```
|
|
926
1015
|
|
|
927
1016
|
- ⚠️ **Do NOT update schema before this step!**
|
|
928
1017
|
- If schema is updated first, the migration will be empty!
|
|
929
1018
|
|
|
930
|
-
|
|
931
|
-
|
|
1019
|
+
7. **Deploy to Forge and verify that the migration runs without issues**
|
|
932
1020
|
- Run the updated migration on Forge.
|
|
933
1021
|
|
|
934
|
-
|
|
1022
|
+
8. **Update the schema**
|
|
935
1023
|
|
|
936
1024
|
```sh
|
|
937
|
-
|
|
1025
|
+
npm run models:create
|
|
938
1026
|
```
|
|
939
1027
|
|
|
940
|
-
|
|
1028
|
+
9. **Repeat steps 5-8 as needed**
|
|
941
1029
|
|
|
942
1030
|
**⚠️ WARNING:**
|
|
943
1031
|
|
|
@@ -947,6 +1035,7 @@ where Cache context - allows you to batch cache invalidation events and bypass c
|
|
|
947
1035
|
## Drop Migrations
|
|
948
1036
|
|
|
949
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
|
+
|
|
950
1039
|
- Start fresh with a new schema
|
|
951
1040
|
- Reset all tables and their data
|
|
952
1041
|
- Clear migration history
|
|
@@ -955,6 +1044,7 @@ The Drop Migrations feature allows you to completely reset your database schema
|
|
|
955
1044
|
### Important Requirements
|
|
956
1045
|
|
|
957
1046
|
Before using Drop Migrations, ensure that:
|
|
1047
|
+
|
|
958
1048
|
1. Your local schema exactly matches the current database schema deployed in Atlassian Forge SQL
|
|
959
1049
|
2. You have a backup of your data if needed
|
|
960
1050
|
3. You understand that this operation will delete all tables and data
|
|
@@ -962,16 +1052,21 @@ Before using Drop Migrations, ensure that:
|
|
|
962
1052
|
### Usage
|
|
963
1053
|
|
|
964
1054
|
1. First, ensure your local schema matches the deployed database:
|
|
1055
|
+
|
|
965
1056
|
```bash
|
|
966
|
-
|
|
1057
|
+
npm run models:create
|
|
967
1058
|
```
|
|
968
1059
|
|
|
969
1060
|
2. Generate the drop migration:
|
|
1061
|
+
|
|
970
1062
|
```bash
|
|
971
|
-
|
|
1063
|
+
npm run migration:drop
|
|
972
1064
|
```
|
|
973
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
|
+
|
|
974
1068
|
3. Deploy and run the migration in your Forge app:
|
|
1069
|
+
|
|
975
1070
|
```js
|
|
976
1071
|
import migrationRunner from "./database/migration";
|
|
977
1072
|
import { MigrationRunner } from "@forge/sql/out/migration";
|
|
@@ -983,13 +1078,14 @@ Before using Drop Migrations, ensure that:
|
|
|
983
1078
|
|
|
984
1079
|
4. After dropping all tables, you can create a new migration to recreate the schema:
|
|
985
1080
|
```bash
|
|
986
|
-
|
|
1081
|
+
npm run migration:create
|
|
987
1082
|
```
|
|
988
|
-
The `--force` parameter is
|
|
1083
|
+
The `--force` parameter is already included in the script to allow creating migrations after dropping all tables.
|
|
989
1084
|
|
|
990
1085
|
### Example Migration Output
|
|
991
1086
|
|
|
992
1087
|
The generated drop migration will look like this:
|
|
1088
|
+
|
|
993
1089
|
```js
|
|
994
1090
|
import { MigrationRunner } from "@forge/sql/out/migration";
|
|
995
1091
|
|
|
@@ -1017,31 +1113,36 @@ export default (migrationRunner: MigrationRunner): MigrationRunner => {
|
|
|
1017
1113
|
|
|
1018
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:
|
|
1019
1115
|
|
|
1020
|
-
| Date type | Required Format
|
|
1021
|
-
|
|
1022
|
-
| DATE
|
|
1023
|
-
| TIME
|
|
1116
|
+
| Date type | Required Format | Example |
|
|
1117
|
+
| --------- | ------------------------------ | -------------------------- |
|
|
1118
|
+
| DATE | YYYY-MM-DD | 2024-09-19 |
|
|
1119
|
+
| TIME | HH:MM:SS[.fraction] | 06:40:34 |
|
|
1024
1120
|
| TIMESTAMP | YYYY-MM-DD HH:MM:SS[.fraction] | 2024-09-19 06:40:34.999999 |
|
|
1025
1121
|
|
|
1026
1122
|
```typescript
|
|
1027
1123
|
// ❌ Don't use standard Drizzle date/time types
|
|
1028
|
-
export const testEntityTimeStampVersion = mysqlTable(
|
|
1029
|
-
id: int(
|
|
1030
|
-
time_stamp: timestamp(
|
|
1031
|
-
date_time: datetime(
|
|
1032
|
-
time: time(
|
|
1033
|
-
date: date(
|
|
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(),
|
|
1034
1130
|
});
|
|
1035
1131
|
|
|
1036
1132
|
// ✅ Use Forge-SQL-ORM custom types instead
|
|
1037
|
-
import {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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(),
|
|
1045
1146
|
});
|
|
1046
1147
|
```
|
|
1047
1148
|
|
|
@@ -1057,6 +1158,7 @@ const timestamp = moment().format("YYYY-MM-DDTHH:mm:ss.SSS");
|
|
|
1057
1158
|
```
|
|
1058
1159
|
|
|
1059
1160
|
Our custom types provide:
|
|
1161
|
+
|
|
1060
1162
|
- Automatic conversion between JavaScript Date objects and Forge SQL's required string formats
|
|
1061
1163
|
- Consistent date/time handling across your application
|
|
1062
1164
|
- Type safety for date/time fields
|
|
@@ -1072,9 +1174,6 @@ Our custom types provide:
|
|
|
1072
1174
|
|
|
1073
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.
|
|
1074
1176
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
1177
|
# Connection to ORM
|
|
1079
1178
|
|
|
1080
1179
|
```js
|
|
@@ -1082,7 +1181,8 @@ import ForgeSQL from "forge-sql-orm";
|
|
|
1082
1181
|
|
|
1083
1182
|
const forgeSQL = new ForgeSQL();
|
|
1084
1183
|
```
|
|
1085
|
-
|
|
1184
|
+
|
|
1185
|
+
or
|
|
1086
1186
|
|
|
1087
1187
|
```typescript
|
|
1088
1188
|
import { drizzle } from "drizzle-orm/mysql-proxy";
|
|
@@ -1101,71 +1201,47 @@ const users = await db.select().from(users);
|
|
|
1101
1201
|
|
|
1102
1202
|
```js
|
|
1103
1203
|
// Using forgeSQL.select()
|
|
1104
|
-
const user = await forgeSQL
|
|
1105
|
-
.select({user: users})
|
|
1106
|
-
.from(users);
|
|
1204
|
+
const user = await forgeSQL.select({ user: users }).from(users);
|
|
1107
1205
|
|
|
1108
1206
|
// Using forgeSQL.selectDistinct()
|
|
1109
|
-
const user = await forgeSQL
|
|
1110
|
-
.selectDistinct({user: users})
|
|
1111
|
-
.from(users);
|
|
1207
|
+
const user = await forgeSQL.selectDistinct({ user: users }).from(users);
|
|
1112
1208
|
|
|
1113
1209
|
// Using forgeSQL.selectCacheable()
|
|
1114
|
-
const user = await forgeSQL
|
|
1115
|
-
.selectCacheable({user: users})
|
|
1116
|
-
.from(users);
|
|
1210
|
+
const user = await forgeSQL.selectCacheable({ user: users }).from(users);
|
|
1117
1211
|
|
|
1118
1212
|
// Using forgeSQL.selectFrom() - Select all columns with field aliasing
|
|
1119
|
-
const user = await forgeSQL
|
|
1120
|
-
.selectFrom(users)
|
|
1121
|
-
.where(eq(users.id, 1));
|
|
1213
|
+
const user = await forgeSQL.selectFrom(users).where(eq(users.id, 1));
|
|
1122
1214
|
|
|
1123
1215
|
// Using forgeSQL.selectDistinctFrom() - Select distinct all columns with field aliasing
|
|
1124
|
-
const user = await forgeSQL
|
|
1125
|
-
.selectDistinctFrom(users)
|
|
1126
|
-
.where(eq(users.id, 1));
|
|
1216
|
+
const user = await forgeSQL.selectDistinctFrom(users).where(eq(users.id, 1));
|
|
1127
1217
|
|
|
1128
1218
|
// Using forgeSQL.selectCacheableFrom() - Select all columns with field aliasing and caching
|
|
1129
|
-
const user = await forgeSQL
|
|
1130
|
-
.selectCacheableFrom(users)
|
|
1131
|
-
.where(eq(users.id, 1));
|
|
1219
|
+
const user = await forgeSQL.selectCacheableFrom(users).where(eq(users.id, 1));
|
|
1132
1220
|
|
|
1133
1221
|
// Using forgeSQL.selectDistinctCacheableFrom() - Select distinct all columns with field aliasing and caching
|
|
1134
|
-
const user = await forgeSQL
|
|
1135
|
-
.selectDistinctCacheableFrom(users)
|
|
1136
|
-
.where(eq(users.id, 1));
|
|
1222
|
+
const user = await forgeSQL.selectDistinctCacheableFrom(users).where(eq(users.id, 1));
|
|
1137
1223
|
|
|
1138
1224
|
// Using forgeSQL.execute() - Execute raw SQL with local caching
|
|
1139
|
-
const user = await forgeSQL
|
|
1140
|
-
.execute("SELECT * FROM users WHERE id = ?", [1]);
|
|
1225
|
+
const user = await forgeSQL.execute("SELECT * FROM users WHERE id = ?", [1]);
|
|
1141
1226
|
|
|
1142
1227
|
// Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
|
|
1143
1228
|
// ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
|
|
1144
1229
|
// Example: SELECT * FROM `users` WHERE id = ? (NOT: SELECT * FROM users WHERE id = ?)
|
|
1145
|
-
const user = await forgeSQL
|
|
1146
|
-
.executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
|
|
1230
|
+
const user = await forgeSQL.executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
|
|
1147
1231
|
|
|
1148
1232
|
// Using forgeSQL.getDrizzleQueryBuilder()
|
|
1149
|
-
const user = await forgeSQL
|
|
1150
|
-
.getDrizzleQueryBuilder()
|
|
1151
|
-
.select().from(Users)
|
|
1152
|
-
.where(eq(Users.id, 1));
|
|
1233
|
+
const user = await forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1));
|
|
1153
1234
|
|
|
1154
1235
|
// OR using direct drizzle with custom driver
|
|
1155
1236
|
const db = drizzle(forgeDriver);
|
|
1156
|
-
const user = await db
|
|
1157
|
-
.select().from(Users)
|
|
1158
|
-
.where(eq(Users.id, 1));
|
|
1237
|
+
const user = await db.select().from(Users).where(eq(Users.id, 1));
|
|
1159
1238
|
// Returns: { id: 1, name: "John Doe" }
|
|
1160
1239
|
|
|
1161
1240
|
// Using executeQueryOnlyOne for single result with error handling
|
|
1162
1241
|
const user = await forgeSQL
|
|
1163
1242
|
.fetch()
|
|
1164
1243
|
.executeQueryOnlyOne(
|
|
1165
|
-
forgeSQL
|
|
1166
|
-
.getDrizzleQueryBuilder()
|
|
1167
|
-
.select().from(Users)
|
|
1168
|
-
.where(eq(Users.id, 1))
|
|
1244
|
+
forgeSQL.getDrizzleQueryBuilder().select().from(Users).where(eq(Users.id, 1)),
|
|
1169
1245
|
);
|
|
1170
1246
|
// Returns: { id: 1, name: "John Doe" }
|
|
1171
1247
|
// Throws error if multiple records found
|
|
@@ -1177,26 +1253,29 @@ const usersAlias = alias(Users, "u");
|
|
|
1177
1253
|
const result = await forgeSQL
|
|
1178
1254
|
.getDrizzleQueryBuilder()
|
|
1179
1255
|
.select({
|
|
1180
|
-
userId: sql<string
|
|
1181
|
-
userName: sql<string
|
|
1182
|
-
})
|
|
1256
|
+
userId: sql < string > `${usersAlias.id} as \`userId\``,
|
|
1257
|
+
userName: sql < string > `${usersAlias.name} as \`userName\``,
|
|
1258
|
+
})
|
|
1259
|
+
.from(usersAlias);
|
|
1183
1260
|
|
|
1184
1261
|
// OR with direct drizzle
|
|
1185
1262
|
const db = drizzle(forgeDriver);
|
|
1186
1263
|
const result = await db
|
|
1187
1264
|
.select({
|
|
1188
|
-
userId: sql<string
|
|
1189
|
-
userName: sql<string
|
|
1190
|
-
})
|
|
1265
|
+
userId: sql < string > `${usersAlias.id} as \`userId\``,
|
|
1266
|
+
userName: sql < string > `${usersAlias.name} as \`userName\``,
|
|
1267
|
+
})
|
|
1268
|
+
.from(usersAlias);
|
|
1191
1269
|
// Returns: { userId: 1, userName: "John Doe" }
|
|
1192
1270
|
```
|
|
1193
1271
|
|
|
1194
1272
|
### Complex Queries
|
|
1273
|
+
|
|
1195
1274
|
```js
|
|
1196
1275
|
// Using joins with automatic field name collision prevention
|
|
1197
1276
|
// With forgeSQL
|
|
1198
1277
|
const orderWithUser = await forgeSQL
|
|
1199
|
-
.select({user: users, order: orders})
|
|
1278
|
+
.select({ user: users, order: orders })
|
|
1200
1279
|
.from(orders)
|
|
1201
1280
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
1202
1281
|
|
|
@@ -1215,12 +1294,12 @@ const orderWithUser = await forgeSQL
|
|
|
1215
1294
|
// Using with() for Common Table Expressions (CTEs)
|
|
1216
1295
|
const userStats = await forgeSQL
|
|
1217
1296
|
.with(
|
|
1218
|
-
forgeSQL.selectFrom(users).where(eq(users.active, true)).as(
|
|
1219
|
-
forgeSQL.selectFrom(orders).where(eq(orders.status,
|
|
1297
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
|
|
1298
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
|
|
1220
1299
|
)
|
|
1221
1300
|
.select({
|
|
1222
1301
|
totalActiveUsers: sql`COUNT(au.id)`,
|
|
1223
|
-
totalCompletedOrders: sql`COUNT(co.id)
|
|
1302
|
+
totalCompletedOrders: sql`COUNT(co.id)`,
|
|
1224
1303
|
})
|
|
1225
1304
|
.from(sql`activeUsers au`)
|
|
1226
1305
|
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
@@ -1228,33 +1307,30 @@ const userStats = await forgeSQL
|
|
|
1228
1307
|
// OR with direct drizzle
|
|
1229
1308
|
const db = patchDbWithSelectAliased(drizzle(forgeDriver));
|
|
1230
1309
|
const orderWithUser = await db
|
|
1231
|
-
.selectAliased({user: users, order: orders})
|
|
1310
|
+
.selectAliased({ user: users, order: orders })
|
|
1232
1311
|
.from(orders)
|
|
1233
1312
|
.innerJoin(users, eq(orders.userId, users.id));
|
|
1234
|
-
// Returns: {
|
|
1235
|
-
// user_id: 1,
|
|
1313
|
+
// Returns: {
|
|
1314
|
+
// user_id: 1,
|
|
1236
1315
|
// user_name: "John Doe",
|
|
1237
1316
|
// order_id: 1,
|
|
1238
1317
|
// order_product: "Product 1"
|
|
1239
1318
|
// }
|
|
1240
1319
|
|
|
1241
1320
|
// Using distinct with aliases
|
|
1242
|
-
const uniqueUsers = await db
|
|
1243
|
-
.selectAliasedDistinct({user: users})
|
|
1244
|
-
.from(users);
|
|
1321
|
+
const uniqueUsers = await db.selectAliasedDistinct({ user: users }).from(users);
|
|
1245
1322
|
// Returns unique users with aliased fields
|
|
1246
1323
|
|
|
1247
1324
|
// Using executeQueryOnlyOne for unique results
|
|
1248
|
-
const userStats = await forgeSQL
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
);
|
|
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
|
+
);
|
|
1258
1334
|
// Returns: { totalUsers: 100, uniqueNames: 80 }
|
|
1259
1335
|
// Throws error if multiple records found
|
|
1260
1336
|
```
|
|
@@ -1286,14 +1362,14 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
1286
1362
|
},
|
|
1287
1363
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
1288
1364
|
const threshold = 500; // ms baseline for this resolver
|
|
1289
|
-
|
|
1365
|
+
|
|
1290
1366
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
1291
1367
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
1292
1368
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
1293
1369
|
} else if (totalDbExecutionTime > threshold) {
|
|
1294
1370
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
1295
1371
|
}
|
|
1296
|
-
|
|
1372
|
+
|
|
1297
1373
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
1298
1374
|
}
|
|
1299
1375
|
);
|
|
@@ -1308,7 +1384,7 @@ await forgeSQL.executeDDL(`
|
|
|
1308
1384
|
`);
|
|
1309
1385
|
|
|
1310
1386
|
await forgeSQL.executeDDL(sql`
|
|
1311
|
-
ALTER TABLE users
|
|
1387
|
+
ALTER TABLE users
|
|
1312
1388
|
ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
1313
1389
|
`);
|
|
1314
1390
|
|
|
@@ -1319,23 +1395,23 @@ await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
|
|
|
1319
1395
|
await forgeSQL.executeDDLActions(async () => {
|
|
1320
1396
|
// Execute regular SQL queries in DDL context for performance monitoring
|
|
1321
1397
|
const slowQueries = await forgeSQL.execute(`
|
|
1322
|
-
SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
|
|
1398
|
+
SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
|
|
1323
1399
|
WHERE AVG_LATENCY > 1000000
|
|
1324
1400
|
`);
|
|
1325
|
-
|
|
1401
|
+
|
|
1326
1402
|
// Execute complex analysis queries in DDL context
|
|
1327
1403
|
const performanceData = await forgeSQL.execute(`
|
|
1328
1404
|
SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
|
|
1329
1405
|
WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
|
1330
1406
|
`);
|
|
1331
|
-
|
|
1407
|
+
|
|
1332
1408
|
return { slowQueries, performanceData };
|
|
1333
1409
|
});
|
|
1334
1410
|
|
|
1335
1411
|
// Using execute() with complex queries
|
|
1336
1412
|
const userStats = await forgeSQL
|
|
1337
1413
|
.execute(`
|
|
1338
|
-
SELECT
|
|
1414
|
+
SELECT
|
|
1339
1415
|
u.id,
|
|
1340
1416
|
u.name,
|
|
1341
1417
|
COUNT(o.id) as order_count,
|
|
@@ -1360,13 +1436,10 @@ These operations work like standard Drizzle methods but participate in cache con
|
|
|
1360
1436
|
await forgeSQL.insert(Users).values({ id: 1, name: "Smith" });
|
|
1361
1437
|
|
|
1362
1438
|
// Basic update (participates in cache context when used within executeWithCacheContext)
|
|
1363
|
-
await forgeSQL.update(Users)
|
|
1364
|
-
.set({ name: "Smith Updated" })
|
|
1365
|
-
.where(eq(Users.id, 1));
|
|
1439
|
+
await forgeSQL.update(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
|
|
1366
1440
|
|
|
1367
1441
|
// Basic delete (participates in cache context when used within executeWithCacheContext)
|
|
1368
|
-
await forgeSQL.delete(Users)
|
|
1369
|
-
.where(eq(Users.id, 1));
|
|
1442
|
+
await forgeSQL.delete(Users).where(eq(Users.id, 1));
|
|
1370
1443
|
```
|
|
1371
1444
|
|
|
1372
1445
|
### 2. Non-Versioned Operations with Cache Management
|
|
@@ -1378,13 +1451,10 @@ These operations don't use optimistic locking but provide cache invalidation:
|
|
|
1378
1451
|
await forgeSQL.insertAndEvictCache(Users).values({ id: 1, name: "Smith" });
|
|
1379
1452
|
|
|
1380
1453
|
// Update without versioning but with cache invalidation
|
|
1381
|
-
await forgeSQL.updateAndEvictCache(Users)
|
|
1382
|
-
.set({ name: "Smith Updated" })
|
|
1383
|
-
.where(eq(Users.id, 1));
|
|
1454
|
+
await forgeSQL.updateAndEvictCache(Users).set({ name: "Smith Updated" }).where(eq(Users.id, 1));
|
|
1384
1455
|
|
|
1385
1456
|
// Delete without versioning but with cache invalidation
|
|
1386
|
-
await forgeSQL.deleteAndEvictCache(Users)
|
|
1387
|
-
.where(eq(Users.id, 1));
|
|
1457
|
+
await forgeSQL.deleteAndEvictCache(Users).where(eq(Users.id, 1));
|
|
1388
1458
|
```
|
|
1389
1459
|
|
|
1390
1460
|
### 3. Versioned Operations with Cache Management (Recommended)
|
|
@@ -1393,16 +1463,20 @@ These operations use optimistic locking and automatic cache invalidation:
|
|
|
1393
1463
|
|
|
1394
1464
|
```js
|
|
1395
1465
|
// Insert with versioning and cache management
|
|
1396
|
-
const userId = await forgeSQL
|
|
1466
|
+
const userId = await forgeSQL
|
|
1467
|
+
.modifyWithVersioningAndEvictCache()
|
|
1468
|
+
.insert(Users, [{ id: 1, name: "Smith" }]);
|
|
1397
1469
|
|
|
1398
1470
|
// Bulk insert with versioning
|
|
1399
1471
|
await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1472
|
+
{ id: 2, name: "Smith" },
|
|
1473
|
+
{ id: 3, name: "Vasyl" },
|
|
1474
|
+
]);
|
|
1403
1475
|
|
|
1404
1476
|
// Update by ID with optimistic locking and cache invalidation
|
|
1405
|
-
await forgeSQL
|
|
1477
|
+
await forgeSQL
|
|
1478
|
+
.modifyWithVersioningAndEvictCache()
|
|
1479
|
+
.updateById({ id: 1, name: "Smith Updated" }, Users);
|
|
1406
1480
|
|
|
1407
1481
|
// Delete by ID with versioning and cache invalidation
|
|
1408
1482
|
await forgeSQL.modifyWithVersioningAndEvictCache().deleteById(1, Users);
|
|
@@ -1446,18 +1520,16 @@ await forgeSQL.modifyWithVersioning().deleteById(1, Users);
|
|
|
1446
1520
|
import { nextVal } from "forge-sql-orm";
|
|
1447
1521
|
|
|
1448
1522
|
const user = {
|
|
1449
|
-
id: nextVal(
|
|
1523
|
+
id: nextVal("user_id_seq"),
|
|
1450
1524
|
name: "user test",
|
|
1451
|
-
organization_id: 1
|
|
1525
|
+
organization_id: 1,
|
|
1452
1526
|
};
|
|
1453
1527
|
const id = await forgeSQL.modifyWithVersioning().insert(appUser, [user]);
|
|
1454
1528
|
|
|
1455
1529
|
// Update with custom WHERE condition
|
|
1456
|
-
await forgeSQL
|
|
1457
|
-
|
|
1458
|
-
Users,
|
|
1459
|
-
eq(Users.email, "smith@example.com")
|
|
1460
|
-
);
|
|
1530
|
+
await forgeSQL
|
|
1531
|
+
.modifyWithVersioning()
|
|
1532
|
+
.updateFields({ name: "New Name", age: 35 }, Users, eq(Users.email, "smith@example.com"));
|
|
1461
1533
|
|
|
1462
1534
|
// Insert with duplicate handling
|
|
1463
1535
|
await forgeSQL.modifyWithVersioning().insert(
|
|
@@ -1466,7 +1538,7 @@ await forgeSQL.modifyWithVersioning().insert(
|
|
|
1466
1538
|
{ id: 4, name: "Smith" },
|
|
1467
1539
|
{ id: 4, name: "Vasyl" },
|
|
1468
1540
|
],
|
|
1469
|
-
true
|
|
1541
|
+
true,
|
|
1470
1542
|
);
|
|
1471
1543
|
```
|
|
1472
1544
|
|
|
@@ -1488,19 +1560,21 @@ const result = await forgeSQL
|
|
|
1488
1560
|
.offset(formatLimitOffset(350000));
|
|
1489
1561
|
|
|
1490
1562
|
// The generated SQL will be:
|
|
1491
|
-
// SELECT * FROM order_item
|
|
1492
|
-
// ORDER BY created_at ASC
|
|
1493
|
-
// LIMIT 10
|
|
1563
|
+
// SELECT * FROM order_item
|
|
1564
|
+
// ORDER BY created_at ASC
|
|
1565
|
+
// LIMIT 10
|
|
1494
1566
|
// OFFSET 350000
|
|
1495
1567
|
```
|
|
1496
1568
|
|
|
1497
1569
|
**Important Notes:**
|
|
1570
|
+
|
|
1498
1571
|
- The function performs type checking to prevent SQL injection
|
|
1499
1572
|
- It throws an error if the input is not a valid number
|
|
1500
1573
|
- Use this function instead of direct parameter binding for LIMIT and OFFSET clauses
|
|
1501
1574
|
- The function is specifically designed to work with Atlassian Forge SQL's limitations
|
|
1502
1575
|
|
|
1503
1576
|
**Security Considerations:**
|
|
1577
|
+
|
|
1504
1578
|
- The function includes validation to ensure the input is a valid number
|
|
1505
1579
|
- This prevents SQL injection by ensuring only numeric values are inserted
|
|
1506
1580
|
- Always use this function instead of string concatenation for LIMIT and OFFSET values
|
|
@@ -1534,9 +1608,9 @@ const options = {
|
|
|
1534
1608
|
tableName: "users",
|
|
1535
1609
|
versionField: {
|
|
1536
1610
|
fieldName: "updatedAt",
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1540
1614
|
};
|
|
1541
1615
|
|
|
1542
1616
|
const forgeSQL = new ForgeSQL(options);
|
|
@@ -1559,7 +1633,6 @@ The caching system leverages Forge's Custom entity store to provide:
|
|
|
1559
1633
|
// Value: { data: [...], expiration: 1234567890, sql: "select * from 1" }
|
|
1560
1634
|
```
|
|
1561
1635
|
|
|
1562
|
-
|
|
1563
1636
|
### Cache Context Operations
|
|
1564
1637
|
|
|
1565
1638
|
The cache context allows you to batch cache invalidation events and bypass cache reads for affected tables:
|
|
@@ -1620,72 +1693,78 @@ Local cache is an in-memory caching layer that operates within a single resolver
|
|
|
1620
1693
|
// Execute operations within a local cache context
|
|
1621
1694
|
await forgeSQL.executeWithLocalContext(async () => {
|
|
1622
1695
|
// First call - executes query and caches result
|
|
1623
|
-
const users = await forgeSQL
|
|
1624
|
-
.
|
|
1625
|
-
|
|
1696
|
+
const users = await forgeSQL
|
|
1697
|
+
.select({ id: users.id, name: users.name })
|
|
1698
|
+
.from(users)
|
|
1699
|
+
.where(eq(users.active, true));
|
|
1700
|
+
|
|
1626
1701
|
// Second call - gets result from local cache (no database query)
|
|
1627
|
-
const cachedUsers = await forgeSQL
|
|
1628
|
-
.
|
|
1629
|
-
|
|
1630
|
-
// Using new selectFrom methods with local caching
|
|
1631
|
-
const usersFrom = await forgeSQL.selectFrom(users)
|
|
1702
|
+
const cachedUsers = await forgeSQL
|
|
1703
|
+
.select({ id: users.id, name: users.name })
|
|
1704
|
+
.from(users)
|
|
1632
1705
|
.where(eq(users.active, true));
|
|
1633
|
-
|
|
1706
|
+
|
|
1707
|
+
// Using new selectFrom methods with local caching
|
|
1708
|
+
const usersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
1709
|
+
|
|
1634
1710
|
// This will use local cache (no database call)
|
|
1635
|
-
const cachedUsersFrom = await forgeSQL.selectFrom(users)
|
|
1636
|
-
|
|
1637
|
-
|
|
1711
|
+
const cachedUsersFrom = await forgeSQL.selectFrom(users).where(eq(users.active, true));
|
|
1712
|
+
|
|
1638
1713
|
// Using execute() with local caching
|
|
1639
|
-
const rawUsers = await forgeSQL.execute(
|
|
1640
|
-
|
|
1641
|
-
[true]
|
|
1642
|
-
);
|
|
1643
|
-
|
|
1714
|
+
const rawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]);
|
|
1715
|
+
|
|
1644
1716
|
// This will use local cache (no database call)
|
|
1645
|
-
const cachedRawUsers = await forgeSQL.execute(
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1717
|
+
const cachedRawUsers = await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [
|
|
1718
|
+
true,
|
|
1719
|
+
]);
|
|
1720
|
+
|
|
1650
1721
|
// Raw SQL with execution metadata and performance monitoring
|
|
1651
1722
|
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
1652
1723
|
async () => {
|
|
1653
1724
|
const users = await forgeSQL.selectFrom(usersTable);
|
|
1654
|
-
const orders = await forgeSQL
|
|
1725
|
+
const orders = await forgeSQL
|
|
1726
|
+
.selectFrom(ordersTable)
|
|
1727
|
+
.where(eq(ordersTable.userId, usersTable.id));
|
|
1655
1728
|
return { users, orders };
|
|
1656
1729
|
},
|
|
1657
1730
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
1658
1731
|
const threshold = 500; // ms baseline for this resolver
|
|
1659
|
-
|
|
1732
|
+
|
|
1660
1733
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
1661
1734
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
1662
1735
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
1663
1736
|
} else if (totalDbExecutionTime > threshold) {
|
|
1664
1737
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
1665
1738
|
}
|
|
1666
|
-
|
|
1739
|
+
|
|
1667
1740
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
1668
|
-
}
|
|
1741
|
+
},
|
|
1669
1742
|
);
|
|
1670
|
-
|
|
1743
|
+
|
|
1671
1744
|
// Insert operation - evicts local cache for users table
|
|
1672
|
-
await forgeSQL.insert(users).values({ name:
|
|
1673
|
-
|
|
1745
|
+
await forgeSQL.insert(users).values({ name: "New User", active: true });
|
|
1746
|
+
|
|
1674
1747
|
// Third call - executes query again and caches new result
|
|
1675
|
-
const updatedUsers = await forgeSQL
|
|
1676
|
-
.
|
|
1748
|
+
const updatedUsers = await forgeSQL
|
|
1749
|
+
.select({ id: users.id, name: users.name })
|
|
1750
|
+
.from(users)
|
|
1751
|
+
.where(eq(users.active, true));
|
|
1677
1752
|
});
|
|
1678
1753
|
|
|
1679
1754
|
// Execute with return value
|
|
1680
1755
|
const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
|
|
1681
1756
|
// First call - executes query and caches result
|
|
1682
|
-
const users = await forgeSQL
|
|
1683
|
-
.
|
|
1684
|
-
|
|
1757
|
+
const users = await forgeSQL
|
|
1758
|
+
.select({ id: users.id, name: users.name })
|
|
1759
|
+
.from(users)
|
|
1760
|
+
.where(eq(users.active, true));
|
|
1761
|
+
|
|
1685
1762
|
// Second call - gets result from local cache (no database query)
|
|
1686
|
-
const cachedUsers = await forgeSQL
|
|
1687
|
-
.
|
|
1688
|
-
|
|
1763
|
+
const cachedUsers = await forgeSQL
|
|
1764
|
+
.select({ id: users.id, name: users.name })
|
|
1765
|
+
.from(users)
|
|
1766
|
+
.where(eq(users.active, true));
|
|
1767
|
+
|
|
1689
1768
|
return { users, cachedUsers };
|
|
1690
1769
|
});
|
|
1691
1770
|
```
|
|
@@ -1697,57 +1776,57 @@ const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async (
|
|
|
1697
1776
|
const userResolver = async (req) => {
|
|
1698
1777
|
return await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
|
|
1699
1778
|
// Get user details using selectFrom (all columns with field aliasing)
|
|
1700
|
-
const user = await forgeSQL.selectFrom(users)
|
|
1701
|
-
|
|
1702
|
-
|
|
1779
|
+
const user = await forgeSQL.selectFrom(users).where(eq(users.id, args.userId));
|
|
1780
|
+
|
|
1703
1781
|
// Get user's orders using selectCacheableFrom (with caching)
|
|
1704
|
-
const orders = await forgeSQL.selectCacheableFrom(orders)
|
|
1705
|
-
|
|
1706
|
-
|
|
1782
|
+
const orders = await forgeSQL.selectCacheableFrom(orders).where(eq(orders.userId, args.userId));
|
|
1783
|
+
|
|
1707
1784
|
// Get user's profile using raw SQL with execute()
|
|
1708
1785
|
const profile = await forgeSQL.execute(
|
|
1709
|
-
"SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
|
|
1710
|
-
[args.userId]
|
|
1786
|
+
"SELECT id, bio, avatar FROM profiles WHERE user_id = ?",
|
|
1787
|
+
[args.userId],
|
|
1711
1788
|
);
|
|
1712
|
-
|
|
1789
|
+
|
|
1713
1790
|
// Get user statistics using complex raw SQL
|
|
1714
|
-
const stats = await forgeSQL.execute(
|
|
1791
|
+
const stats = await forgeSQL.execute(
|
|
1792
|
+
`
|
|
1715
1793
|
SELECT
|
|
1716
1794
|
COUNT(o.id) as total_orders,
|
|
1717
1795
|
SUM(o.amount) as total_spent,
|
|
1718
1796
|
AVG(o.amount) as avg_order_value
|
|
1719
1797
|
FROM orders o
|
|
1720
1798
|
WHERE o.user_id = ? AND o.status = 'completed'
|
|
1721
|
-
`,
|
|
1722
|
-
|
|
1799
|
+
`,
|
|
1800
|
+
[args.userId],
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1723
1803
|
// If any of these queries are repeated within the same resolver,
|
|
1724
1804
|
// they will use the local cache instead of hitting the database
|
|
1725
|
-
|
|
1805
|
+
|
|
1726
1806
|
return {
|
|
1727
1807
|
...user[0],
|
|
1728
1808
|
orders,
|
|
1729
1809
|
profile: profile[0],
|
|
1730
|
-
stats: stats[0]
|
|
1810
|
+
stats: stats[0],
|
|
1731
1811
|
};
|
|
1732
1812
|
});
|
|
1733
1813
|
};
|
|
1734
1814
|
```
|
|
1735
1815
|
|
|
1736
|
-
|
|
1737
1816
|
#### Local Cache (Level 1) vs Global Cache (Level 2)
|
|
1738
1817
|
|
|
1739
|
-
| Feature
|
|
1740
|
-
|
|
1741
|
-
| **Storage**
|
|
1742
|
-
| **Scope**
|
|
1743
|
-
| **Persistence**
|
|
1744
|
-
| **Performance**
|
|
1745
|
-
| **Memory Usage**
|
|
1746
|
-
| **Use Case**
|
|
1747
|
-
| **Configuration**
|
|
1748
|
-
| **TTL Support**
|
|
1749
|
-
| **Cache Eviction** | Automatic on DML operations
|
|
1750
|
-
| **Best For**
|
|
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 |
|
|
1751
1830
|
|
|
1752
1831
|
#### Integration with Global Cache (Level 2)
|
|
1753
1832
|
|
|
@@ -1760,19 +1839,20 @@ await forgeSQL.executeWithLocalContext(async () => {
|
|
|
1760
1839
|
// 1. Local cache (Level 1 - in-memory)
|
|
1761
1840
|
// 2. Global cache (Level 2 - KVS)
|
|
1762
1841
|
// 3. Database query
|
|
1763
|
-
const users = await forgeSQL
|
|
1764
|
-
.
|
|
1765
|
-
|
|
1766
|
-
// Using new methods with multi-level caching
|
|
1767
|
-
const usersFrom = await forgeSQL.selectCacheableFrom(users)
|
|
1842
|
+
const users = await forgeSQL
|
|
1843
|
+
.selectCacheable({ id: users.id, name: users.name })
|
|
1844
|
+
.from(users)
|
|
1768
1845
|
.where(eq(users.active, true));
|
|
1769
|
-
|
|
1846
|
+
|
|
1847
|
+
// Using new methods with multi-level caching
|
|
1848
|
+
const usersFrom = await forgeSQL.selectCacheableFrom(users).where(eq(users.active, true));
|
|
1849
|
+
|
|
1770
1850
|
// Raw SQL with multi-level caching
|
|
1771
1851
|
// ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
|
|
1772
1852
|
const rawUsers = await forgeSQL.executeCacheable(
|
|
1773
|
-
"SELECT id, name FROM `users` WHERE active = ?",
|
|
1774
|
-
[true],
|
|
1775
|
-
300 // TTL in seconds
|
|
1853
|
+
"SELECT id, name FROM `users` WHERE active = ?",
|
|
1854
|
+
[true],
|
|
1855
|
+
300, // TTL in seconds
|
|
1776
1856
|
);
|
|
1777
1857
|
});
|
|
1778
1858
|
```
|
|
@@ -1796,26 +1876,26 @@ The diagram below shows how local cache works in Forge-SQL-ORM:
|
|
|
1796
1876
|
// Execute queries with caching
|
|
1797
1877
|
const users = await forgeSQL.modifyWithVersioningAndEvictCache().executeQuery(
|
|
1798
1878
|
forgeSQL.select().from(Users).where(eq(Users.active, true)),
|
|
1799
|
-
600 // Custom TTL in seconds
|
|
1879
|
+
600, // Custom TTL in seconds
|
|
1800
1880
|
);
|
|
1801
1881
|
|
|
1802
1882
|
// Execute single result queries with caching
|
|
1803
|
-
const user = await forgeSQL
|
|
1804
|
-
|
|
1805
|
-
);
|
|
1883
|
+
const user = await forgeSQL
|
|
1884
|
+
.modifyWithVersioningAndEvictCache()
|
|
1885
|
+
.executeQueryOnlyOne(forgeSQL.select().from(Users).where(eq(Users.id, 1)));
|
|
1806
1886
|
|
|
1807
1887
|
// Execute raw SQL with caching
|
|
1808
1888
|
const results = await forgeSQL.modifyWithVersioningAndEvictCache().executeRawSQL(
|
|
1809
1889
|
"SELECT * FROM users WHERE active = ?",
|
|
1810
1890
|
[true],
|
|
1811
|
-
300 // TTL in seconds
|
|
1891
|
+
300, // TTL in seconds
|
|
1812
1892
|
);
|
|
1813
1893
|
|
|
1814
1894
|
// Using new methods for cache-aware operations
|
|
1815
|
-
const usersFrom = await forgeSQL.selectCacheableFrom(Users)
|
|
1816
|
-
.where(eq(Users.active, true));
|
|
1895
|
+
const usersFrom = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
|
|
1817
1896
|
|
|
1818
|
-
const usersDistinct = await forgeSQL
|
|
1897
|
+
const usersDistinct = await forgeSQL
|
|
1898
|
+
.selectDistinctCacheableFrom(Users)
|
|
1819
1899
|
.where(eq(Users.active, true));
|
|
1820
1900
|
|
|
1821
1901
|
// Raw SQL with local and global caching
|
|
@@ -1823,18 +1903,18 @@ const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
|
|
|
1823
1903
|
const rawUsers = await forgeSQL.executeCacheable(
|
|
1824
1904
|
"SELECT * FROM `users` WHERE active = ?",
|
|
1825
1905
|
[true],
|
|
1826
|
-
300 // TTL in seconds
|
|
1906
|
+
300, // TTL in seconds
|
|
1827
1907
|
);
|
|
1828
1908
|
|
|
1829
1909
|
// Using with() for Common Table Expressions with caching
|
|
1830
1910
|
const userStats = await forgeSQL
|
|
1831
1911
|
.with(
|
|
1832
|
-
forgeSQL.selectFrom(users).where(eq(users.active, true)).as(
|
|
1833
|
-
forgeSQL.selectFrom(orders).where(eq(orders.status,
|
|
1912
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
|
|
1913
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
|
|
1834
1914
|
)
|
|
1835
1915
|
.select({
|
|
1836
1916
|
totalActiveUsers: sql`COUNT(au.id)`,
|
|
1837
|
-
totalCompletedOrders: sql`COUNT(co.id)
|
|
1917
|
+
totalCompletedOrders: sql`COUNT(co.id)`,
|
|
1838
1918
|
})
|
|
1839
1919
|
.from(sql`activeUsers au`)
|
|
1840
1920
|
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
@@ -1843,21 +1923,23 @@ const userStats = await forgeSQL
|
|
|
1843
1923
|
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
1844
1924
|
async () => {
|
|
1845
1925
|
const users = await forgeSQL.selectFrom(usersTable);
|
|
1846
|
-
const orders = await forgeSQL
|
|
1926
|
+
const orders = await forgeSQL
|
|
1927
|
+
.selectFrom(ordersTable)
|
|
1928
|
+
.where(eq(ordersTable.userId, usersTable.id));
|
|
1847
1929
|
return { users, orders };
|
|
1848
1930
|
},
|
|
1849
1931
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
1850
1932
|
const threshold = 500; // ms baseline for this resolver
|
|
1851
|
-
|
|
1933
|
+
|
|
1852
1934
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
1853
1935
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
1854
1936
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
1855
1937
|
} else if (totalDbExecutionTime > threshold) {
|
|
1856
1938
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
1857
1939
|
}
|
|
1858
|
-
|
|
1940
|
+
|
|
1859
1941
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
1860
|
-
}
|
|
1942
|
+
},
|
|
1861
1943
|
);
|
|
1862
1944
|
```
|
|
1863
1945
|
|
|
@@ -1893,9 +1975,9 @@ const options = {
|
|
|
1893
1975
|
tableName: "users",
|
|
1894
1976
|
versionField: {
|
|
1895
1977
|
fieldName: "updatedAt",
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1978
|
+
},
|
|
1979
|
+
},
|
|
1980
|
+
},
|
|
1899
1981
|
};
|
|
1900
1982
|
|
|
1901
1983
|
const forgeSQL = new ForgeSQL(options);
|
|
@@ -1906,24 +1988,26 @@ const forgeSQL = new ForgeSQL(options);
|
|
|
1906
1988
|
```typescript
|
|
1907
1989
|
// The version field will be automatically handled
|
|
1908
1990
|
await forgeSQL.modifyWithVersioning().updateById(
|
|
1909
|
-
{
|
|
1910
|
-
id: 1,
|
|
1991
|
+
{
|
|
1992
|
+
id: 1,
|
|
1911
1993
|
name: "Updated Name",
|
|
1912
|
-
updatedAt: new Date() // Will be automatically set if not provided
|
|
1913
|
-
},
|
|
1914
|
-
Users
|
|
1994
|
+
updatedAt: new Date(), // Will be automatically set if not provided
|
|
1995
|
+
},
|
|
1996
|
+
Users,
|
|
1915
1997
|
);
|
|
1916
1998
|
```
|
|
1999
|
+
|
|
1917
2000
|
or with cache support
|
|
2001
|
+
|
|
1918
2002
|
```typescript
|
|
1919
2003
|
// The version field will be automatically handled
|
|
1920
2004
|
await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
|
|
1921
|
-
{
|
|
1922
|
-
id: 1,
|
|
2005
|
+
{
|
|
2006
|
+
id: 1,
|
|
1923
2007
|
name: "Updated Name",
|
|
1924
|
-
updatedAt: new Date() // Will be automatically set if not provided
|
|
1925
|
-
},
|
|
1926
|
-
Users
|
|
2008
|
+
updatedAt: new Date(), // Will be automatically set if not provided
|
|
2009
|
+
},
|
|
2010
|
+
Users,
|
|
1927
2011
|
);
|
|
1928
2012
|
```
|
|
1929
2013
|
|
|
@@ -1931,16 +2015,16 @@ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
|
|
|
1931
2015
|
|
|
1932
2016
|
The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
|
|
1933
2017
|
|
|
1934
|
-
| Option | Type | Description
|
|
1935
|
-
| -------------------------- | --------- |
|
|
1936
|
-
| `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`.
|
|
1937
|
-
| `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`. |
|
|
1938
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. |
|
|
1939
|
-
| `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.
|
|
1940
|
-
| `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"`.
|
|
1941
|
-
| `cacheTTL` | `number` | Default cache TTL in seconds. Defaults to `120` (2 minutes).
|
|
1942
|
-
| `cacheWrapTable` | `boolean` | Whether to wrap table names with backticks in cache keys. Defaults to `true`.
|
|
1943
|
-
| `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. |
|
|
1944
2028
|
|
|
1945
2029
|
## CLI Commands
|
|
1946
2030
|
|
|
@@ -1959,23 +2043,39 @@ The CLI tool provides the following main commands:
|
|
|
1959
2043
|
|
|
1960
2044
|
### Installation
|
|
1961
2045
|
|
|
2046
|
+
The CLI tool must be installed as a local dependency and used via npm scripts in your `package.json`:
|
|
2047
|
+
|
|
1962
2048
|
```bash
|
|
1963
|
-
npm install
|
|
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
|
+
|
|
2056
|
+
```bash
|
|
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"
|
|
1964
2060
|
```
|
|
1965
2061
|
|
|
1966
2062
|
### Basic Usage
|
|
1967
2063
|
|
|
2064
|
+
After setting up the scripts, use them via npm:
|
|
2065
|
+
|
|
1968
2066
|
```bash
|
|
1969
2067
|
# Generate models from database
|
|
1970
|
-
|
|
2068
|
+
npm run models:create
|
|
1971
2069
|
|
|
1972
2070
|
# Create migration
|
|
1973
|
-
|
|
2071
|
+
npm run migration:create
|
|
1974
2072
|
|
|
1975
2073
|
# Update migration
|
|
1976
|
-
|
|
2074
|
+
npm run migration:update
|
|
1977
2075
|
```
|
|
1978
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
|
+
|
|
1979
2079
|
For detailed information about all available options and advanced usage, see the [Full CLI Documentation](forge-sql-orm-cli/README.md).
|
|
1980
2080
|
|
|
1981
2081
|
## Web Triggers for Migrations
|
|
@@ -1985,7 +2085,8 @@ Forge-SQL-ORM provides web triggers for managing database migrations in Atlassia
|
|
|
1985
2085
|
### 1. Apply Migrations Trigger
|
|
1986
2086
|
|
|
1987
2087
|
This trigger allows you to apply database migrations through a web endpoint. It's useful for:
|
|
1988
|
-
|
|
2088
|
+
|
|
2089
|
+
- Manually triggering migrations
|
|
1989
2090
|
- Running migrations as part of your deployment process
|
|
1990
2091
|
- Testing migrations in different environments
|
|
1991
2092
|
|
|
@@ -2000,22 +2101,23 @@ export const handlerMigration = async () => {
|
|
|
2000
2101
|
```
|
|
2001
2102
|
|
|
2002
2103
|
Configure in `manifest.yml`:
|
|
2104
|
+
|
|
2003
2105
|
```yaml
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
|
2019
2121
|
```
|
|
2020
2122
|
|
|
2021
2123
|
### 2. Drop Migrations Trigger
|
|
@@ -2023,11 +2125,12 @@ Configure in `manifest.yml`:
|
|
|
2023
2125
|
⚠️ **WARNING**: This trigger will permanently delete all data in the specified tables and clear the migrations history. This operation cannot be undone!
|
|
2024
2126
|
|
|
2025
2127
|
This trigger allows you to completely reset your database schema. It's useful for:
|
|
2128
|
+
|
|
2026
2129
|
- Development environments where you need to start fresh
|
|
2027
2130
|
- Testing scenarios requiring a clean database
|
|
2028
2131
|
- Resetting the database before applying new migrations
|
|
2029
2132
|
|
|
2030
|
-
**Important**: The trigger will
|
|
2133
|
+
**Important**: The trigger will drop all tables including migration.
|
|
2031
2134
|
|
|
2032
2135
|
```typescript
|
|
2033
2136
|
// Example usage in your Forge app
|
|
@@ -2039,16 +2142,17 @@ export const dropMigrations = () => {
|
|
|
2039
2142
|
```
|
|
2040
2143
|
|
|
2041
2144
|
Configure in `manifest.yml`:
|
|
2145
|
+
|
|
2042
2146
|
```yaml
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
|
2052
2156
|
```
|
|
2053
2157
|
|
|
2054
2158
|
### 3. Fetch Schema Trigger
|
|
@@ -2056,12 +2160,14 @@ Configure in `manifest.yml`:
|
|
|
2056
2160
|
⚠️ **DEVELOPMENT ONLY**: This trigger is designed for development environments only and should not be used in production.
|
|
2057
2161
|
|
|
2058
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
|
+
|
|
2059
2164
|
- Development environment setup
|
|
2060
2165
|
- Schema documentation
|
|
2061
2166
|
- Database structure verification
|
|
2062
2167
|
- Creating backup scripts
|
|
2063
2168
|
|
|
2064
2169
|
**Security Considerations**:
|
|
2170
|
+
|
|
2065
2171
|
- This trigger exposes your database structure
|
|
2066
2172
|
- It temporarily disables foreign key checks
|
|
2067
2173
|
- It may expose sensitive table names and structures
|
|
@@ -2077,19 +2183,21 @@ export const fetchSchema = async () => {
|
|
|
2077
2183
|
```
|
|
2078
2184
|
|
|
2079
2185
|
Configure in `manifest.yml`:
|
|
2186
|
+
|
|
2080
2187
|
```yaml
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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
|
|
2090
2197
|
```
|
|
2091
2198
|
|
|
2092
2199
|
The response will contain SQL statements like:
|
|
2200
|
+
|
|
2093
2201
|
```sql
|
|
2094
2202
|
SET foreign_key_checks = 0;
|
|
2095
2203
|
CREATE TABLE IF NOT EXISTS users (...);
|
|
@@ -2100,6 +2208,7 @@ SET foreign_key_checks = 1;
|
|
|
2100
2208
|
### 4. Clear Cache Scheduler Trigger
|
|
2101
2209
|
|
|
2102
2210
|
This trigger automatically cleans up expired cache entries based on their TTL (Time To Live). It's useful for:
|
|
2211
|
+
|
|
2103
2212
|
- Automatic cache maintenance
|
|
2104
2213
|
- Preventing cache storage from growing indefinitely
|
|
2105
2214
|
- Ensuring optimal cache performance
|
|
@@ -2110,24 +2219,26 @@ This trigger automatically cleans up expired cache entries based on their TTL (T
|
|
|
2110
2219
|
import { clearCacheSchedulerTrigger } from "forge-sql-orm";
|
|
2111
2220
|
|
|
2112
2221
|
export const clearCache = () => {
|
|
2113
|
-
return clearCacheSchedulerTrigger({
|
|
2222
|
+
return clearCacheSchedulerTrigger({
|
|
2114
2223
|
cacheEntityName: "cache",
|
|
2115
2224
|
});
|
|
2116
2225
|
};
|
|
2117
2226
|
```
|
|
2118
2227
|
|
|
2119
2228
|
Configure in `manifest.yml`:
|
|
2229
|
+
|
|
2120
2230
|
```yaml
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2231
|
+
scheduledTrigger:
|
|
2232
|
+
- key: clear-cache-trigger
|
|
2233
|
+
function: clearCache
|
|
2234
|
+
interval: fiveMinute
|
|
2235
|
+
function:
|
|
2236
|
+
- key: clearCache
|
|
2237
|
+
handler: index.clearCache
|
|
2128
2238
|
```
|
|
2129
2239
|
|
|
2130
2240
|
**Available Intervals**:
|
|
2241
|
+
|
|
2131
2242
|
- `fiveMinute` - Every 5 minutes
|
|
2132
2243
|
- `hour` - Every hour
|
|
2133
2244
|
- `day` - Every day
|
|
@@ -2148,14 +2259,15 @@ export const slowQueryTrigger = () =>
|
|
|
2148
2259
|
```
|
|
2149
2260
|
|
|
2150
2261
|
Configure in `manifest.yml`:
|
|
2262
|
+
|
|
2151
2263
|
```yaml
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2264
|
+
scheduledTrigger:
|
|
2265
|
+
- key: slow-query-trigger
|
|
2266
|
+
function: slowQueryTrigger
|
|
2267
|
+
interval: hour
|
|
2268
|
+
function:
|
|
2269
|
+
- key: slowQueryTrigger
|
|
2270
|
+
handler: index.slowQueryTrigger
|
|
2159
2271
|
```
|
|
2160
2272
|
|
|
2161
2273
|
> **💡 Note**: For complete documentation, examples, and configuration options, see the [Slow Query Monitoring](#slow-query-monitoring) section.
|
|
@@ -2163,15 +2275,17 @@ Configure in `manifest.yml`:
|
|
|
2163
2275
|
### Important Notes
|
|
2164
2276
|
|
|
2165
2277
|
**Security Considerations**:
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
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
|
|
2169
2282
|
|
|
2170
2283
|
**Best Practices**:
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
|
2175
2289
|
|
|
2176
2290
|
## Query Analysis and Performance Optimization
|
|
2177
2291
|
|
|
@@ -2182,6 +2296,7 @@ Forge-SQL-ORM provides comprehensive query analysis tools to help you optimize y
|
|
|
2182
2296
|
### About Atlassian's Built-in Analysis Tools
|
|
2183
2297
|
|
|
2184
2298
|
Atlassian provides comprehensive query analysis tools in the development console, including:
|
|
2299
|
+
|
|
2185
2300
|
- Basic query performance metrics
|
|
2186
2301
|
- Slow query tracking (queries over 500ms)
|
|
2187
2302
|
- Basic execution statistics
|
|
@@ -2281,8 +2396,8 @@ modules:
|
|
|
2281
2396
|
scheduledTrigger:
|
|
2282
2397
|
- key: slow-query-trigger
|
|
2283
2398
|
function: slowQueryTrigger
|
|
2284
|
-
interval: hour
|
|
2285
|
-
|
|
2399
|
+
interval: hour # Run every hour
|
|
2400
|
+
|
|
2286
2401
|
function:
|
|
2287
2402
|
- key: slowQueryTrigger
|
|
2288
2403
|
handler: index.slowQueryTrigger
|
|
@@ -2290,10 +2405,10 @@ modules:
|
|
|
2290
2405
|
|
|
2291
2406
|
#### Configuration Options
|
|
2292
2407
|
|
|
2293
|
-
| Option
|
|
2294
|
-
|
|
2295
|
-
| `hours`
|
|
2296
|
-
| `timeout` | `number` | `3000`
|
|
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 |
|
|
2297
2412
|
|
|
2298
2413
|
#### Example Console Output
|
|
2299
2414
|
|
|
@@ -2374,59 +2489,57 @@ const analyzeForgeSql = forgeSQL.analyze();
|
|
|
2374
2489
|
|
|
2375
2490
|
// Analyze a Drizzle query
|
|
2376
2491
|
const plan = await analyzeForgeSql.explain(
|
|
2377
|
-
forgeSQL
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
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)),
|
|
2389
2505
|
);
|
|
2390
2506
|
|
|
2391
2507
|
// Analyze a raw SQL query
|
|
2392
|
-
const rawPlan = await analyzeForgeSql.explainRaw(
|
|
2393
|
-
"SELECT * FROM users WHERE id = ?",
|
|
2394
|
-
[1]
|
|
2395
|
-
);
|
|
2508
|
+
const rawPlan = await analyzeForgeSql.explainRaw("SELECT * FROM users WHERE id = ?", [1]);
|
|
2396
2509
|
|
|
2397
2510
|
// Analyze new methods
|
|
2398
2511
|
const usersFromPlan = await analyzeForgeSql.explain(
|
|
2399
|
-
forgeSQL.selectFrom(users).where(eq(users.active, true))
|
|
2512
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)),
|
|
2400
2513
|
);
|
|
2401
2514
|
|
|
2402
2515
|
const usersCacheablePlan = await analyzeForgeSql.explain(
|
|
2403
|
-
forgeSQL.selectCacheableFrom(users).where(eq(users.active, true))
|
|
2516
|
+
forgeSQL.selectCacheableFrom(users).where(eq(users.active, true)),
|
|
2404
2517
|
);
|
|
2405
2518
|
|
|
2406
2519
|
// Analyze Common Table Expressions (CTEs)
|
|
2407
2520
|
const ctePlan = await analyzeForgeSql.explain(
|
|
2408
2521
|
forgeSQL
|
|
2409
2522
|
.with(
|
|
2410
|
-
forgeSQL.selectFrom(users).where(eq(users.active, true)).as(
|
|
2411
|
-
forgeSQL.selectFrom(orders).where(eq(orders.status,
|
|
2523
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
|
|
2524
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
|
|
2412
2525
|
)
|
|
2413
2526
|
.select({
|
|
2414
2527
|
totalActiveUsers: sql`COUNT(au.id)`,
|
|
2415
|
-
totalCompletedOrders: sql`COUNT(co.id)
|
|
2528
|
+
totalCompletedOrders: sql`COUNT(co.id)`,
|
|
2416
2529
|
})
|
|
2417
2530
|
.from(sql`activeUsers au`)
|
|
2418
|
-
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`))
|
|
2531
|
+
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`)),
|
|
2419
2532
|
);
|
|
2420
2533
|
```
|
|
2421
2534
|
|
|
2422
2535
|
This analysis provides insights into:
|
|
2536
|
+
|
|
2423
2537
|
- How the database executes your query
|
|
2424
2538
|
- Which indexes are being used
|
|
2425
2539
|
- Estimated vs actual row counts
|
|
2426
2540
|
- Resource usage at each step
|
|
2427
2541
|
- Performance optimization opportunities
|
|
2428
2542
|
|
|
2429
|
-
|
|
2430
2543
|
## Migration Guide
|
|
2431
2544
|
|
|
2432
2545
|
### Migrating from 2.0.x to 2.1.x
|
|
@@ -2436,18 +2549,20 @@ This section covers the breaking changes introduced in version 2.1.x and how to
|
|
|
2436
2549
|
#### 1. Method Renaming (BREAKING CHANGES)
|
|
2437
2550
|
|
|
2438
2551
|
**Removed Methods:**
|
|
2552
|
+
|
|
2439
2553
|
- `forgeSQL.modify()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
|
|
2440
2554
|
- `forgeSQL.crud()` → **REMOVED** (use `forgeSQL.modifyWithVersioning()`)
|
|
2441
2555
|
|
|
2442
2556
|
**Migration Steps:**
|
|
2443
2557
|
|
|
2444
2558
|
1. **Replace `modify()` calls:**
|
|
2559
|
+
|
|
2445
2560
|
```typescript
|
|
2446
2561
|
// ❌ Old (2.0.x) - NO LONGER WORKS
|
|
2447
2562
|
await forgeSQL.modify().insert(Users, [userData]);
|
|
2448
2563
|
await forgeSQL.modify().updateById(updateData, Users);
|
|
2449
2564
|
await forgeSQL.modify().deleteById(1, Users);
|
|
2450
|
-
|
|
2565
|
+
|
|
2451
2566
|
// ✅ New (2.1.x) - REQUIRED
|
|
2452
2567
|
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
2453
2568
|
await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
|
|
@@ -2455,12 +2570,13 @@ This section covers the breaking changes introduced in version 2.1.x and how to
|
|
|
2455
2570
|
```
|
|
2456
2571
|
|
|
2457
2572
|
2. **Replace `crud()` calls:**
|
|
2573
|
+
|
|
2458
2574
|
```typescript
|
|
2459
2575
|
// ❌ Old (2.0.x) - NO LONGER WORKS
|
|
2460
2576
|
await forgeSQL.crud().insert(Users, [userData]);
|
|
2461
2577
|
await forgeSQL.crud().updateById(updateData, Users);
|
|
2462
2578
|
await forgeSQL.crud().deleteById(1, Users);
|
|
2463
|
-
|
|
2579
|
+
|
|
2464
2580
|
// ✅ New (2.1.x) - REQUIRED
|
|
2465
2581
|
await forgeSQL.modifyWithVersioning().insert(Users, [userData]);
|
|
2466
2582
|
await forgeSQL.modifyWithVersioning().updateById(updateData, Users);
|
|
@@ -2470,8 +2586,9 @@ This section covers the breaking changes introduced in version 2.1.x and how to
|
|
|
2470
2586
|
#### 2. New API Methods
|
|
2471
2587
|
|
|
2472
2588
|
**New Methods Available:**
|
|
2589
|
+
|
|
2473
2590
|
- `forgeSQL.insert()` - Basic Drizzle operations
|
|
2474
|
-
- `forgeSQL.update()` - Basic Drizzle operations
|
|
2591
|
+
- `forgeSQL.update()` - Basic Drizzle operations
|
|
2475
2592
|
- `forgeSQL.delete()` - Basic Drizzle operations
|
|
2476
2593
|
- `forgeSQL.insertAndEvictCache()` - Basic Drizzle operations with evict cache after execution
|
|
2477
2594
|
- `forgeSQL.updateAndEvictCache()` - Basic Drizzle operations with evict cache after execution
|
|
@@ -2499,47 +2616,43 @@ await forgeSQL.insert(Users).values(userData);
|
|
|
2499
2616
|
await forgeSQL.modifyWithVersioningAndEvictCache().insert(Users, [userData]);
|
|
2500
2617
|
|
|
2501
2618
|
// ✅ New query methods for better performance
|
|
2502
|
-
const users = await forgeSQL.selectFrom(Users)
|
|
2503
|
-
.where(eq(Users.active, true));
|
|
2619
|
+
const users = await forgeSQL.selectFrom(Users).where(eq(Users.active, true));
|
|
2504
2620
|
|
|
2505
|
-
const usersDistinct = await forgeSQL.selectDistinctFrom(Users)
|
|
2506
|
-
.where(eq(Users.active, true));
|
|
2621
|
+
const usersDistinct = await forgeSQL.selectDistinctFrom(Users).where(eq(Users.active, true));
|
|
2507
2622
|
|
|
2508
|
-
const usersCacheable = await forgeSQL.selectCacheableFrom(Users)
|
|
2509
|
-
.where(eq(Users.active, true));
|
|
2623
|
+
const usersCacheable = await forgeSQL.selectCacheableFrom(Users).where(eq(Users.active, true));
|
|
2510
2624
|
|
|
2511
2625
|
// ✅ Raw SQL execution with caching
|
|
2512
|
-
const rawUsers = await forgeSQL.execute(
|
|
2513
|
-
"SELECT * FROM users WHERE active = ?",
|
|
2514
|
-
[true]
|
|
2515
|
-
);
|
|
2626
|
+
const rawUsers = await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]);
|
|
2516
2627
|
|
|
2517
2628
|
// ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
|
|
2518
2629
|
const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
2519
|
-
"SELECT * FROM `users` WHERE active = ?",
|
|
2520
|
-
[true],
|
|
2521
|
-
300
|
|
2630
|
+
"SELECT * FROM `users` WHERE active = ?",
|
|
2631
|
+
[true],
|
|
2632
|
+
300,
|
|
2522
2633
|
);
|
|
2523
2634
|
|
|
2524
2635
|
// ✅ Raw SQL execution with metadata capture and performance monitoring
|
|
2525
2636
|
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
2526
2637
|
async () => {
|
|
2527
2638
|
const users = await forgeSQL.selectFrom(usersTable);
|
|
2528
|
-
const orders = await forgeSQL
|
|
2639
|
+
const orders = await forgeSQL
|
|
2640
|
+
.selectFrom(ordersTable)
|
|
2641
|
+
.where(eq(ordersTable.userId, usersTable.id));
|
|
2529
2642
|
return { users, orders };
|
|
2530
2643
|
},
|
|
2531
2644
|
(totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2532
2645
|
const threshold = 500; // ms baseline for this resolver
|
|
2533
|
-
|
|
2646
|
+
|
|
2534
2647
|
if (totalDbExecutionTime > threshold * 1.5) {
|
|
2535
2648
|
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
2536
2649
|
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
2537
2650
|
} else if (totalDbExecutionTime > threshold) {
|
|
2538
2651
|
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
2539
2652
|
}
|
|
2540
|
-
|
|
2653
|
+
|
|
2541
2654
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
2542
|
-
}
|
|
2655
|
+
},
|
|
2543
2656
|
);
|
|
2544
2657
|
|
|
2545
2658
|
// ✅ DDL operations for schema modifications
|
|
@@ -2563,25 +2676,25 @@ await forgeSQL.executeDDLActions(async () => {
|
|
|
2563
2676
|
SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
|
|
2564
2677
|
WHERE AVG_LATENCY > 1000000
|
|
2565
2678
|
`);
|
|
2566
|
-
|
|
2679
|
+
|
|
2567
2680
|
// Execute complex analysis queries in DDL context
|
|
2568
2681
|
const performanceData = await forgeSQL.execute(`
|
|
2569
2682
|
SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
|
|
2570
2683
|
WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
|
2571
2684
|
`);
|
|
2572
|
-
|
|
2685
|
+
|
|
2573
2686
|
return { slowQueries, performanceData };
|
|
2574
2687
|
});
|
|
2575
2688
|
|
|
2576
2689
|
// ✅ Common Table Expressions (CTEs)
|
|
2577
2690
|
const userStats = await forgeSQL
|
|
2578
2691
|
.with(
|
|
2579
|
-
forgeSQL.selectFrom(users).where(eq(users.active, true)).as(
|
|
2580
|
-
forgeSQL.selectFrom(orders).where(eq(orders.status,
|
|
2692
|
+
forgeSQL.selectFrom(users).where(eq(users.active, true)).as("activeUsers"),
|
|
2693
|
+
forgeSQL.selectFrom(orders).where(eq(orders.status, "completed")).as("completedOrders"),
|
|
2581
2694
|
)
|
|
2582
2695
|
.select({
|
|
2583
2696
|
totalActiveUsers: sql`COUNT(au.id)`,
|
|
2584
|
-
totalCompletedOrders: sql`COUNT(co.id)
|
|
2697
|
+
totalCompletedOrders: sql`COUNT(co.id)`,
|
|
2585
2698
|
})
|
|
2586
2699
|
.from(sql`activeUsers au`)
|
|
2587
2700
|
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
@@ -2595,7 +2708,7 @@ You can use a simple find-and-replace to migrate your code:
|
|
|
2595
2708
|
# Replace modify() calls
|
|
2596
2709
|
find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.modify()/forgeSQL.modifyWithVersioning()/g'
|
|
2597
2710
|
|
|
2598
|
-
# Replace crud() calls
|
|
2711
|
+
# Replace crud() calls
|
|
2599
2712
|
find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.modifyWithVersioning()/g'
|
|
2600
2713
|
```
|
|
2601
2714
|
|
|
@@ -2607,5 +2720,6 @@ find . -name "*.ts" -o -name "*.js" | xargs sed -i 's/forgeSQL\.crud()/forgeSQL.
|
|
|
2607
2720
|
- ✅ **Migration Required**: You must update your code to use the new methods
|
|
2608
2721
|
|
|
2609
2722
|
## License
|
|
2723
|
+
|
|
2610
2724
|
This project is licensed under the **MIT License**.
|
|
2611
2725
|
Feel free to use it for commercial and personal projects.
|