forge-sql-orm 2.1.10 → 2.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +356 -263
  2. package/dist/ForgeSQLORM.js +3263 -3226
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +3261 -3224
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLORM.d.ts +66 -12
  7. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.d.ts +66 -11
  9. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  10. package/dist/core/SystemTables.d.ts +82 -82
  11. package/dist/utils/cacheUtils.d.ts.map +1 -1
  12. package/dist/utils/forgeDriver.d.ts.map +1 -1
  13. package/dist/utils/forgeDriverProxy.d.ts +6 -2
  14. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  15. package/dist/utils/metadataContextUtils.d.ts +5 -2
  16. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  17. package/dist/utils/sqlUtils.d.ts +72 -1
  18. package/dist/utils/sqlUtils.d.ts.map +1 -1
  19. package/dist/webtriggers/index.d.ts +1 -1
  20. package/dist/webtriggers/index.d.ts.map +1 -1
  21. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts +67 -0
  22. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -0
  23. package/package.json +12 -12
  24. package/src/core/ForgeSQLORM.ts +166 -29
  25. package/src/core/ForgeSQLQueryBuilder.ts +65 -11
  26. package/src/core/SystemTables.ts +1 -1
  27. package/src/utils/cacheUtils.ts +26 -3
  28. package/src/utils/forgeDriver.ts +15 -34
  29. package/src/utils/forgeDriverProxy.ts +58 -6
  30. package/src/utils/metadataContextUtils.ts +18 -6
  31. package/src/utils/sqlUtils.ts +241 -2
  32. package/src/webtriggers/index.ts +1 -1
  33. package/src/webtriggers/slowQuerySchedulerTrigger.ts +89 -0
  34. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +0 -114
  35. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +0 -1
  36. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +0 -563
package/README.md CHANGED
@@ -9,7 +9,6 @@
9
9
 
10
10
  [![forge-sql-orm CI](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml/badge.svg)](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml)
11
11
  [![Coverage Status](https://coveralls.io/repos/github/vzakharchenko/forge-sql-orm/badge.svg?branch=master)](https://coveralls.io/github/vzakharchenko/forge-sql-orm?branch=master)
12
- [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vzakharchenko_forge-sql-orm&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vzakharchenko_forge-sql-orm)
13
12
  [![DeepScan grade](https://deepscan.io/api/teams/26652/projects/29272/branches/940614/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=26652&pid=29272&bid=940614)
14
13
 
15
14
 
@@ -19,11 +18,11 @@
19
18
  - ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
20
19
  - ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
21
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/) )
22
- - ✅ **Performance Monitoring**: Automated detection of memory-intensive queries with configurable thresholds (essential for Atlassian's 16 MiB per query limit)
21
+ - ✅ **Performance Monitoring**: Query execution metrics and analysis capabilities with automatic error analysis for timeout and OOM errors, plus scheduled slow query monitoring with execution plans
23
22
  - ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
24
23
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
25
24
  - ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
26
- - ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and Forge SQL metadata
25
+ - ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and query analysis capabilities with performance monitoring
27
26
  - ✅ **Raw SQL Execution**: `execute()`, `executeCacheable()`, `executeDDL()`, and `executeDDLActions()` methods for direct SQL queries with local and global caching
28
27
  - ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
29
28
  - ✅ **Schema migration support**, allowing automatic schema evolution
@@ -65,7 +64,8 @@
65
64
  ### 🔒 Advanced Features
66
65
  - [Optimistic Locking](#optimistic-locking)
67
66
  - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
68
- - [Performance Monitoring](#performance-monitoring)
67
+ - [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
68
+ - [Slow Query Monitoring](#slow-query-monitoring) - Scheduled monitoring of slow queries with execution plans
69
69
  - [Date and Time Types](#date-and-time-types)
70
70
 
71
71
  ### 🛠️ Development Tools
@@ -82,7 +82,7 @@
82
82
  - [Query Analysis Example](examples/forge-sql-orm-example-query-analyses)
83
83
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
84
84
  - [Checklist Example](examples/forge-sql-orm-example-checklist)
85
- - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with `topSlowestStatementLastHourTrigger` performance monitoring
85
+ - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
86
86
 
87
87
  ### 📚 Reference
88
88
  - [ForgeSqlOrmOptions](#forgesqlormoptions)
@@ -99,7 +99,6 @@
99
99
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
100
100
  - [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
101
101
  - [Optimistic Locking](#optimistic-locking) - Data consistency
102
- - [Memory Usage Monitoring](#memory-usage-monitoring) - Memory-intensive query detection
103
102
  - [Migration Tools](#web-triggers-for-migrations) - Database migrations
104
103
  - [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
105
104
 
@@ -108,7 +107,7 @@
108
107
  - [Optimistic Locking Example](examples/forge-sql-orm-example-optimistic-locking) - Real-world conflict handling
109
108
  - [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
110
109
  - [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
111
- - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching with memory monitoring
110
+ - [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities
112
111
 
113
112
  ## Usage Approaches
114
113
 
@@ -276,7 +275,39 @@ await forgeSQL.executeWithLocalContext(async () => {
276
275
  });
277
276
  ```
278
277
 
279
- ### 4. Next Steps
278
+ ### 4. Resolver Performance Monitoring
279
+ ```typescript
280
+ // Resolver with performance monitoring
281
+ resolver.define("fetch", async (req: Request) => {
282
+ try {
283
+ return await forgeSQL.executeWithMetadata(
284
+ async () => {
285
+ // Resolver logic with multiple queries
286
+ const users = await forgeSQL.selectFrom(demoUsers);
287
+ const orders = await forgeSQL.selectFrom(demoOrders)
288
+ .where(eq(demoOrders.userId, demoUsers.id));
289
+ return { users, orders };
290
+ },
291
+ async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
292
+ const threshold = 500; // ms baseline for this resolver
293
+
294
+ if (totalDbExecutionTime > threshold * 1.5) {
295
+ console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
296
+ await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
297
+ } else if (totalDbExecutionTime > threshold) {
298
+ console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
299
+ }
300
+ }
301
+ );
302
+ } catch (e) {
303
+ const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
304
+ console.error(error, e);
305
+ throw error;
306
+ }
307
+ });
308
+ ```
309
+
310
+ ### 5. Next Steps
280
311
  - [Full Installation Guide](#installation) - Complete setup instructions
281
312
  - [Core Features](#core-features) - Learn about key capabilities
282
313
  - [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
@@ -328,19 +359,31 @@ const rawUsers = await forgeSQL.execute(
328
359
  );
329
360
 
330
361
  // Raw SQL with caching
362
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
331
363
  const cachedRawUsers = await forgeSQL.executeCacheable(
332
- "SELECT * FROM users WHERE active = ?",
364
+ "SELECT * FROM `users` WHERE active = ?",
333
365
  [true],
334
366
  300
335
367
  );
336
368
 
337
- // Raw SQL with execution metadata
369
+ // Raw SQL with execution metadata and performance monitoring
338
370
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
339
- async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
340
- (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
341
- console.log(`DB execution time: ${totalDbExecutionTime}ms`);
342
- console.log(`Response size: ${totalResponseSize} bytes`);
343
- console.log('Forge metadata:', forgeMetadata);
371
+ async () => {
372
+ const users = await forgeSQL.selectFrom(usersTable);
373
+ const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
374
+ return { users, orders };
375
+ },
376
+ (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
377
+ const threshold = 500; // ms baseline for this resolver
378
+
379
+ if (totalDbExecutionTime > threshold * 1.5) {
380
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
381
+ await printQueriesWithPlan(); // Analyze and print query execution plans
382
+ } else if (totalDbExecutionTime > threshold) {
383
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
384
+ }
385
+
386
+ console.log(`DB response size: ${totalResponseSize} bytes`);
344
387
  }
345
388
  );
346
389
 
@@ -438,19 +481,31 @@ const rawUsers = await forgeSQL.execute(
438
481
  );
439
482
 
440
483
  // Raw SQL with caching
484
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
441
485
  const cachedRawUsers = await forgeSQL.executeCacheable(
442
- "SELECT * FROM users WHERE active = ?",
486
+ "SELECT * FROM `users` WHERE active = ?",
443
487
  [true],
444
488
  300
445
489
  );
446
490
 
447
- // Raw SQL with execution metadata
491
+ // Raw SQL with execution metadata and performance monitoring
448
492
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
449
- async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
450
- (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
451
- console.log(`DB execution time: ${totalDbExecutionTime}ms`);
452
- console.log(`Response size: ${totalResponseSize} bytes`);
453
- console.log('Forge metadata:', forgeMetadata);
493
+ async () => {
494
+ const users = await forgeSQL.selectFrom(usersTable);
495
+ const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
496
+ return { users, orders };
497
+ },
498
+ (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
499
+ const threshold = 500; // ms baseline for this resolver
500
+
501
+ if (totalDbExecutionTime > threshold * 1.5) {
502
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
503
+ await printQueriesWithPlan(); // Analyze and print query execution plans
504
+ } else if (totalDbExecutionTime > threshold) {
505
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
506
+ }
507
+
508
+ console.log(`DB response size: ${totalResponseSize} bytes`);
454
509
  }
455
510
  );
456
511
  ```
@@ -563,6 +618,9 @@ Please review the [official @forge/kvs quotas and limits](https://developer.atla
563
618
  - Monitor cache usage to stay within quotas
564
619
  - Use appropriate TTL values
565
620
 
621
+ **⚠️ Important Cache Limitations:**
622
+ - **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. This is by design to support special system tables or temporary tables.
623
+
566
624
  ### Step 1: Install Dependencies
567
625
 
568
626
  ```bash
@@ -1082,8 +1140,10 @@ const user = await forgeSQL
1082
1140
  .execute("SELECT * FROM users WHERE id = ?", [1]);
1083
1141
 
1084
1142
  // Using forgeSQL.executeCacheable() - Execute raw SQL with local and global caching
1143
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
1144
+ // Example: SELECT * FROM `users` WHERE id = ? (NOT: SELECT * FROM users WHERE id = ?)
1085
1145
  const user = await forgeSQL
1086
- .executeCacheable("SELECT * FROM users WHERE id = ?", [1], 300);
1146
+ .executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
1087
1147
 
1088
1148
  // Using forgeSQL.getDrizzleQueryBuilder()
1089
1149
  const user = await forgeSQL
@@ -1212,19 +1272,31 @@ const users = await forgeSQL
1212
1272
  .execute("SELECT * FROM users WHERE active = ?", [true]);
1213
1273
 
1214
1274
  // Using executeCacheable() for raw SQL with local and global caching
1275
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names in SQL queries must be wrapped with backticks (`)
1276
+ // Example: SELECT * FROM `users` WHERE active = ? (NOT: SELECT * FROM users WHERE active = ?)
1215
1277
  const users = await forgeSQL
1216
- .executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
1217
-
1218
- // Using executeWithMetadata() for capturing execution metrics
1219
- const usersWithMetadata = await forgeSQL
1220
- .executeWithMetadata(
1221
- async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
1222
- (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
1223
- console.log(`DB execution time: ${totalDbExecutionTime}ms`);
1224
- console.log(`Response size: ${totalResponseSize} bytes`);
1225
- console.log('Forge metadata:', forgeMetadata);
1226
- }
1227
- );
1278
+ .executeCacheable("SELECT * FROM `users` WHERE active = ?", [true], 300);
1279
+
1280
+ // Using executeWithMetadata() for capturing execution metrics and performance monitoring
1281
+ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1282
+ async () => {
1283
+ const users = await forgeSQL.selectFrom(usersTable);
1284
+ const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1285
+ return { users, orders };
1286
+ },
1287
+ (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1288
+ const threshold = 500; // ms baseline for this resolver
1289
+
1290
+ if (totalDbExecutionTime > threshold * 1.5) {
1291
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1292
+ await printQueriesWithPlan(); // Analyze and print query execution plans
1293
+ } else if (totalDbExecutionTime > threshold) {
1294
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1295
+ }
1296
+
1297
+ console.log(`DB response size: ${totalResponseSize} bytes`);
1298
+ }
1299
+ );
1228
1300
 
1229
1301
  // Using executeDDL() for DDL operations (CREATE, ALTER, DROP, etc.)
1230
1302
  await forgeSQL.executeDDL(`
@@ -1575,13 +1647,24 @@ await forgeSQL.executeWithLocalContext(async () => {
1575
1647
  [true]
1576
1648
  );
1577
1649
 
1578
- // Raw SQL with execution metadata and local caching
1650
+ // Raw SQL with execution metadata and performance monitoring
1579
1651
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1580
- async () => await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]),
1581
- (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
1582
- console.log(`DB execution time: ${totalDbExecutionTime}ms`);
1583
- console.log(`Response size: ${totalResponseSize} bytes`);
1584
- console.log('Forge metadata:', forgeMetadata);
1652
+ async () => {
1653
+ const users = await forgeSQL.selectFrom(usersTable);
1654
+ const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1655
+ return { users, orders };
1656
+ },
1657
+ (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1658
+ const threshold = 500; // ms baseline for this resolver
1659
+
1660
+ if (totalDbExecutionTime > threshold * 1.5) {
1661
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1662
+ await printQueriesWithPlan(); // Analyze and print query execution plans
1663
+ } else if (totalDbExecutionTime > threshold) {
1664
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1665
+ }
1666
+
1667
+ console.log(`DB response size: ${totalResponseSize} bytes`);
1585
1668
  }
1586
1669
  );
1587
1670
 
@@ -1685,8 +1768,9 @@ await forgeSQL.executeWithLocalContext(async () => {
1685
1768
  .where(eq(users.active, true));
1686
1769
 
1687
1770
  // Raw SQL with multi-level caching
1771
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1688
1772
  const rawUsers = await forgeSQL.executeCacheable(
1689
- "SELECT id, name FROM users WHERE active = ?",
1773
+ "SELECT id, name FROM `users` WHERE active = ?",
1690
1774
  [true],
1691
1775
  300 // TTL in seconds
1692
1776
  );
@@ -1735,8 +1819,9 @@ const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1735
1819
  .where(eq(Users.active, true));
1736
1820
 
1737
1821
  // Raw SQL with local and global caching
1822
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1738
1823
  const rawUsers = await forgeSQL.executeCacheable(
1739
- "SELECT * FROM users WHERE active = ?",
1824
+ "SELECT * FROM `users` WHERE active = ?",
1740
1825
  [true],
1741
1826
  300 // TTL in seconds
1742
1827
  );
@@ -1754,13 +1839,24 @@ const userStats = await forgeSQL
1754
1839
  .from(sql`activeUsers au`)
1755
1840
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
1756
1841
 
1757
- // Using executeWithMetadata() for capturing execution metrics with caching
1842
+ // Using executeWithMetadata() for capturing execution metrics with performance monitoring
1758
1843
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
1759
- async () => await forgeSQL.executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300),
1760
- (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
1761
- console.log(`DB execution time: ${totalDbExecutionTime}ms`);
1762
- console.log(`Response size: ${totalResponseSize} bytes`);
1763
- console.log('Forge metadata:', forgeMetadata);
1844
+ async () => {
1845
+ const users = await forgeSQL.selectFrom(usersTable);
1846
+ const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
1847
+ return { users, orders };
1848
+ },
1849
+ (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
1850
+ const threshold = 500; // ms baseline for this resolver
1851
+
1852
+ if (totalDbExecutionTime > threshold * 1.5) {
1853
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
1854
+ await printQueriesWithPlan(); // Analyze and print query execution plans
1855
+ } else if (totalDbExecutionTime > threshold) {
1856
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
1857
+ }
1858
+
1859
+ console.log(`DB response size: ${totalResponseSize} bytes`);
1764
1860
  }
1765
1861
  );
1766
1862
  ```
@@ -2036,6 +2132,34 @@ Configure in `manifest.yml`:
2036
2132
  - `hour` - Every hour
2037
2133
  - `day` - Every day
2038
2134
 
2135
+ ### 5. Slow Query Scheduler Trigger
2136
+
2137
+ This scheduler trigger automatically monitors and analyzes slow queries on a scheduled basis. For detailed information, see the [Slow Query Monitoring](#slow-query-monitoring) section.
2138
+
2139
+ **Quick Setup:**
2140
+
2141
+ ```typescript
2142
+ import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
2143
+
2144
+ const forgeSQL = new ForgeSQL();
2145
+
2146
+ export const slowQueryTrigger = () =>
2147
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 1, timeout: 3000 });
2148
+ ```
2149
+
2150
+ Configure in `manifest.yml`:
2151
+ ```yaml
2152
+ scheduledTrigger:
2153
+ - key: slow-query-trigger
2154
+ function: slowQueryTrigger
2155
+ interval: hour
2156
+ function:
2157
+ - key: slowQueryTrigger
2158
+ handler: index.slowQueryTrigger
2159
+ ```
2160
+
2161
+ > **💡 Note**: For complete documentation, examples, and configuration options, see the [Slow Query Monitoring](#slow-query-monitoring) section.
2162
+
2039
2163
  ### Important Notes
2040
2164
 
2041
2165
  **Security Considerations**:
@@ -2065,6 +2189,170 @@ Atlassian provides comprehensive query analysis tools in the development console
2065
2189
 
2066
2190
  Our analysis tools complement these built-in features by providing additional insights directly from TiDB's system schemas.
2067
2191
 
2192
+ ### Automatic Error Analysis
2193
+
2194
+ Forge-SQL-ORM automatically intercepts and analyzes critical query errors to help you diagnose performance issues. When a query fails due to **timeout** or **out-of-memory** errors, the library automatically:
2195
+
2196
+ 1. **Detects the error type** (SQL_QUERY_TIMEOUT or Out of Memory)
2197
+ 2. **Logs detailed error information** to the Forge Developer Console
2198
+ 3. **Waits for system tables to populate** (200ms delay)
2199
+ 4. **Retrieves and logs the execution plan** for the failed query
2200
+ 5. **Provides performance metrics** including memory usage, execution time, and query details
2201
+
2202
+ This automatic analysis happens transparently - no additional code is required on your part.
2203
+
2204
+ #### Supported Error Types
2205
+
2206
+ - **SQL_QUERY_TIMEOUT**: Queries that exceed the execution time limit
2207
+ - **Out of Memory (OOM)**: Queries that exceed the 16 MiB memory limit (errno: 8175)
2208
+
2209
+ #### Example Console Output
2210
+
2211
+ When a query fails, you'll see output like this in the Forge Developer Console:
2212
+
2213
+ ```
2214
+ ❌ TIMEOUT detected - Query exceeded time limit
2215
+ ⏳ Waiting 200ms for CLUSTER_STATEMENTS_SUMMARY to populate...
2216
+ 📊 Analyzing query performance and execution plan...
2217
+ ⏱️ Query duration: 10500ms
2218
+
2219
+ SQL: SELECT * FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE u.active = ? | Memory: 12.45 MB | Time: 10500.00 ms | stmtType: Select | Executions: 1
2220
+ Plan:
2221
+ id task estRows operator info actRows execution info memory disk
2222
+ Projection_7 root 1000.00 forge_38dd1c6156b94bb59c2c9a45582bbfc7.users.id, ... 1000 time:10.5s, loops:1 12.45 MB N/A
2223
+ └─IndexHashJoin_14 root 1000.00 inner join, ... 1000 time:10.2s, loops:1 11.98 MB N/A
2224
+ ```
2225
+
2226
+ #### How It Works
2227
+
2228
+ The error analysis mechanism:
2229
+
2230
+ 1. **Error Detection**: When a query fails, the driver proxy checks the error code/errno
2231
+ 2. **Error Logging**: Logs the specific error type to console.error
2232
+ 3. **Data Population Wait**: Waits 200ms for TiDB's `CLUSTER_STATEMENTS_SUMMARY` table to be populated with the failed query's metadata
2233
+ 4. **Query Analysis**: Automatically calls `printQueriesWithPlan()` to retrieve and display:
2234
+ - SQL query text
2235
+ - Memory consumption (average and max in MB)
2236
+ - Execution time (average in ms)
2237
+ - Statement type
2238
+ - Number of executions
2239
+ - Detailed execution plan
2240
+
2241
+ #### Benefits
2242
+
2243
+ - **Zero Configuration**: Works automatically - no setup required
2244
+ - **Immediate Insights**: Get execution plans for failed queries instantly
2245
+ - **Performance Debugging**: Identify bottlenecks without manual investigation
2246
+ - **Development Console Integration**: All logs appear in Atlassian Forge Developer Console
2247
+ - **No Code Changes**: Existing code automatically benefits from error analysis
2248
+
2249
+ > **💡 Tip**: The automatic error analysis only triggers for timeout and OOM errors. Other errors are logged normally without plan analysis.
2250
+
2251
+ ### Slow Query Monitoring
2252
+
2253
+ Forge-SQL-ORM provides a scheduler trigger (`slowQuerySchedulerTrigger`) that automatically monitors and analyzes slow queries on an hourly basis. This trigger queries TiDB's slow query log system table and provides detailed performance information including SQL query text, memory usage, execution time, and execution plans.
2254
+
2255
+ #### Key Features
2256
+
2257
+ - **Automatic Monitoring**: Runs on a scheduled interval (recommended: hourly)
2258
+ - **Detailed Performance Metrics**: Memory usage, execution time, and execution plans
2259
+ - **Console Logging**: Results are automatically logged to the Forge Developer Console
2260
+ - **Configurable Time Window**: Analyze queries from the last N hours (default: 1 hour)
2261
+ - **Automatic Plan Retrieval**: Execution plans are included for all slow queries
2262
+
2263
+ #### Basic Setup
2264
+
2265
+ **1. Create the trigger function:**
2266
+
2267
+ ```typescript
2268
+ import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
2269
+
2270
+ const forgeSQL = new ForgeSQL();
2271
+
2272
+ // Monitor slow queries from the last hour (recommended for hourly schedule)
2273
+ export const slowQueryTrigger = () =>
2274
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 1, timeout: 3000 });
2275
+ ```
2276
+
2277
+ **2. Configure in `manifest.yml`:**
2278
+
2279
+ ```yaml
2280
+ modules:
2281
+ scheduledTrigger:
2282
+ - key: slow-query-trigger
2283
+ function: slowQueryTrigger
2284
+ interval: hour # Run every hour
2285
+
2286
+ function:
2287
+ - key: slowQueryTrigger
2288
+ handler: index.slowQueryTrigger
2289
+ ```
2290
+
2291
+ #### Configuration Options
2292
+
2293
+ | Option | Type | Default | Description |
2294
+ |--------|------|---------|-------------|
2295
+ | `hours` | `number` | `1` | Number of hours to look back for slow queries |
2296
+ | `timeout` | `number` | `3000` | Timeout in milliseconds for the diagnostic query execution |
2297
+
2298
+ #### Example Console Output
2299
+
2300
+ When slow queries are detected, you'll see output like this in the Forge Developer Console:
2301
+
2302
+ ```
2303
+ Found SlowQuery SQL: SELECT * FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE u.active = ? | Memory: 8.50 MB | Time: 2500.00 ms
2304
+ Plan:
2305
+ id task estRows operator info actRows execution info memory disk
2306
+ Projection_7 root 1000.00 forge_38dd1c6156b94bb59c2c9a45582bbfc7.users.id, ... 1000 time:2.5s, loops:1 8.50 MB N/A
2307
+ └─IndexHashJoin_14 root 1000.00 inner join, ... 1000 time:2.2s, loops:1 7.98 MB N/A
2308
+
2309
+ Found SlowQuery SQL: SELECT * FROM products WHERE category = ? ORDER BY created_at DESC | Memory: 6.25 MB | Time: 1800.00 ms
2310
+ Plan:
2311
+ ...
2312
+ ```
2313
+
2314
+ #### Advanced Configuration
2315
+
2316
+ ```typescript
2317
+ import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
2318
+
2319
+ const forgeSQL = new ForgeSQL();
2320
+
2321
+ // Monitor queries from the last 6 hours (for less frequent checks)
2322
+ export const sixHourSlowQueryTrigger = () =>
2323
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 6, timeout: 5000 });
2324
+
2325
+ // Monitor queries from the last 24 hours (daily monitoring)
2326
+ export const dailySlowQueryTrigger = () =>
2327
+ slowQuerySchedulerTrigger(forgeSQL, { hours: 24, timeout: 3000 });
2328
+ ```
2329
+
2330
+ #### How It Works
2331
+
2332
+ 1. **Scheduled Execution**: The trigger runs automatically on the configured interval (hourly recommended)
2333
+ 2. **Query Analysis**: Queries TiDB's slow query log system table for queries executed within the specified time window
2334
+ 3. **Performance Metrics**: Extracts and logs:
2335
+ - SQL query text (sanitized for readability)
2336
+ - Maximum memory usage (in MB)
2337
+ - Query execution time (in ms)
2338
+ - Detailed execution plan
2339
+ 4. **Console Logging**: Results are logged to the Forge Developer Console via `console.warn()` for easy monitoring
2340
+
2341
+ #### Best Practices
2342
+
2343
+ - **Hourly Intervals**: Use `interval: hour` for timely detection of slow queries
2344
+ - **Default Time Window**: 1 hour is recommended for hourly schedules to avoid overlap
2345
+ - **Monitor Regularly**: Check console logs regularly to identify patterns in slow queries
2346
+
2347
+ #### Benefits
2348
+
2349
+ - **Proactive Monitoring**: Catch slow queries before they become critical issues
2350
+ - **Performance Trends**: Track query performance over time
2351
+ - **Optimization Insights**: Execution plans help identify optimization opportunities
2352
+ - **Zero Manual Intervention**: Fully automated monitoring with scheduled execution
2353
+ - **Production Safe**: Works silently in the background, only logs when slow queries are found
2354
+
2355
+ > **💡 Tip**: The trigger queries up to 50 slow queries to prevent excessive logging. Transient timeouts are usually fine; repeated timeouts indicate the diagnostic query itself is slow and should be investigated.
2068
2356
 
2069
2357
  ### Available Analysis Tools
2070
2358
 
@@ -2138,213 +2426,6 @@ This analysis provides insights into:
2138
2426
  - Resource usage at each step
2139
2427
  - Performance optimization opportunities
2140
2428
 
2141
- ## Performance Monitoring
2142
-
2143
- [↑ Back to Top](#table-of-contents)
2144
-
2145
- Forge-SQL-ORM provides automated performance monitoring capabilities to help you identify and track memory-intensive queries in your Forge SQL instance. This feature is **essential for Atlassian Forge applications** as it helps you stay within the **16 MiB per query memory limit** and provides detailed insights for optimization.
2146
-
2147
- ### Why Performance Monitoring is Critical
2148
-
2149
- Atlassian Forge SQL has a strict **16 MiB memory limit per query**. Unlike slow query detection (which is available in the Forge Developer Console), there's **no built-in way to monitor memory usage** of your queries. This monitoring system fills that gap by:
2150
-
2151
- - **Detecting memory-intensive queries** before they hit the 16 MiB limit
2152
- - **Providing detailed memory metrics** including average and maximum memory usage
2153
- - **Showing execution plans** to help optimize memory consumption
2154
- - **Configurable thresholds** to match your application's memory requirements
2155
- - **Scheduled monitoring** via Forge scheduler triggers
2156
-
2157
- ### Overview
2158
-
2159
- The performance monitoring system:
2160
- - **Automatically detects memory-intensive queries** based on configurable memory thresholds
2161
- - **Provides detailed memory metrics** including execution time, memory usage, and execution plans
2162
- - **Logs memory issues** to the Forge Developer Console for easy debugging
2163
- - **Supports scheduled monitoring** via Forge scheduler triggers
2164
- - **Filters out system queries** to focus on your application's performance
2165
-
2166
- ### Key Features
2167
-
2168
- - **Memory-Focused Performance Monitoring**: Primary focus on memory usage with configurable thresholds
2169
- - **Atlassian 16 MiB Limit Awareness**: Designed specifically for Forge SQL's memory constraints
2170
- - **Execution Plan Analysis**: Shows detailed query plans to help optimize memory consumption
2171
- - **Configurable Thresholds**: Set custom memory usage thresholds (default: 4MB warning)
2172
- - **Automatic Filtering**: Excludes system queries (`Use`, `Set`, `Show`) and empty queries
2173
- - **Scheduled Monitoring**: Run automatically on configurable intervals
2174
-
2175
- ### Basic Usage
2176
-
2177
- #### 1. Import the Trigger
2178
-
2179
- ```typescript
2180
- import ForgeSQL, { topSlowestStatementLastHourTrigger } from "forge-sql-orm";
2181
- ```
2182
-
2183
- #### 2. Create a Scheduler Function
2184
-
2185
- ```typescript
2186
- import ForgeSQL, { topSlowestStatementLastHourTrigger } from 'forge-sql-orm';
2187
-
2188
- // Initialize ForgeSQL ORM instance
2189
- const forgeSQL = new ForgeSQL();
2190
-
2191
- // Basic usage with default thresholds (300ms latency, 8MB memory warning)
2192
- export const performanceTrigger = () =>
2193
- topSlowestStatementLastHourTrigger(forgeSQL);
2194
-
2195
- // Conservative performance monitoring: 4MB warning (well below 16MB limit)
2196
- export const conservativeMemoryTrigger = () =>
2197
- topSlowestStatementLastHourTrigger(forgeSQL, { memoryThresholdBytes: 4 * 1024 * 1024 });
2198
-
2199
- // Aggressive performance monitoring: 12MB warning (75% of 16MB limit)
2200
- export const aggressiveMemoryTrigger = () =>
2201
- topSlowestStatementLastHourTrigger(forgeSQL, { memoryThresholdBytes: 12 * 1024 * 1024 });
2202
-
2203
- // Memory-only performance monitoring: Only trigger on memory usage (latency effectively disabled)
2204
- export const memoryOnlyTrigger = () =>
2205
- topSlowestStatementLastHourTrigger(forgeSQL, { warnThresholdMs: 10000, memoryThresholdBytes: 4 * 1024 * 1024 });
2206
-
2207
- // Latency-only monitoring: Only trigger on slow queries (memory effectively disabled)
2208
- export const latencyOnlyTrigger = () =>
2209
- topSlowestStatementLastHourTrigger(forgeSQL, { warnThresholdMs: 500, memoryThresholdBytes: 16 * 1024 * 1024 });
2210
-
2211
- // With execution plan in logs
2212
- export const performanceWithPlanTrigger = () =>
2213
- topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true });
2214
- ```
2215
-
2216
- #### 3. Configure in manifest.yml
2217
-
2218
- **As Scheduler Trigger (Recommended for Production):**
2219
- ```yaml
2220
- scheduledTrigger:
2221
- - key: performance-trigger
2222
- function: perfTrigger
2223
- interval: hour # Required: only hour interval is supported
2224
-
2225
- function:
2226
- - key: perfTrigger
2227
- handler: index.performanceTrigger
2228
- ```
2229
-
2230
- **As Web Trigger (Development Only):**
2231
- ```yaml
2232
- webtrigger:
2233
- - key: print-slowest-queries
2234
- function: perfTrigger
2235
-
2236
- function:
2237
- - key: perfTrigger
2238
- handler: index.performanceTrigger
2239
- ```
2240
-
2241
- > **⚠️ Important**: Web triggers are not recommended for production as they violate the "run-on-atlassian" principle. Use scheduler triggers for production monitoring.
2242
-
2243
- ### How It Works
2244
-
2245
- The performance monitoring trigger works differently depending on how it's configured:
2246
-
2247
- #### Scheduler Trigger Mode (Production)
2248
-
2249
- When used as a **scheduler trigger**, the system:
2250
- - **Runs automatically** on the configured interval (hour only)
2251
- - **Logs to Forge Developer Console** only when thresholds are exceeded
2252
- - **No HTTP response** - operates silently in the background
2253
- - **Perfect for production** monitoring without violating "run-on-atlassian"
2254
-
2255
- **Example Console Log Output:**
2256
- ```
2257
- 1. Select avg=3006.03ms max=3006.03ms mem≈0.08MB(max 0.08MB) exec=1
2258
- digest=28344800f90f6c929484e83337404df7e55a660c5f4ce922c4b298ab5e90c425
2259
- sql=select `demo_users` . `id` as `a_userid_id` , `demo_users` . `name` as `a_username_name` , `demo_orders` . `product` as `a_product_product` , `demo_orders` . `id` as `a_productid_id` , `sleep` ( ? ) from `demo_users` inner join `demo_orders` on `demo_orders` . `user_id` = `demo_users` . `id`
2260
-
2261
- full plan:
2262
- id task estRows operator info actRows execution info memory disk
2263
- Projection_7 root 2.50 forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_users.id, forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_users.name, forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_orders.product, forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_orders.id, sleep(?)->Column#7 3 time:3s, loops:2, Concurrency:OFF 1.98 KB N/A
2264
- └─IndexHashJoin_14 root 2.50 inner join, inner:IndexLookUp_11, outer key:forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_users.id, inner key:forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_orders.user_id, equal cond:eq(forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_users.id, forge_38dd1c6156b94bb59c2c9a45582bbfc7.demo_orders.user_id) 3 time:2.11ms, loops:2, inner:{total:1.31ms, concurrency:5, task:1, construct:6.62µs, fetch:1.3ms, build:4.68µs, join:6.14µs} 57.9 KB N/A
2265
- ```
2266
-
2267
- #### Web Trigger Mode (Development)
2268
-
2269
- When used as a **web trigger**, the system:
2270
- - **Runs on-demand** when the web endpoint is called
2271
- - **Returns JSON response** with detailed metrics
2272
- - **Logs to console** AND provides structured data
2273
- - **Useful for development** and debugging
2274
-
2275
- **Example JSON Response:**
2276
- ```json
2277
- {
2278
- "success": true,
2279
- "window": "last_1h",
2280
- "top": 1,
2281
- "warnThresholdMs": 300,
2282
- "memoryThresholdBytes": 8388608,
2283
- "rows": [
2284
- {
2285
- "rank": 1,
2286
- "digest": "abc123...",
2287
- "stmtType": "Select",
2288
- "schemaName": "myapp",
2289
- "execCount": 150,
2290
- "avgLatencyMs": 450.25,
2291
- "maxLatencyMs": 1200.50,
2292
- "minLatencyMs": 200.10,
2293
- "avgProcessTimeMs": 400.20,
2294
- "avgWaitTimeMs": 50.05,
2295
- "avgBackoffTimeMs": 0.00,
2296
- "avgMemMB": 2.5,
2297
- "maxMemMB": 8.2,
2298
- "avgMemBytes": 2621440,
2299
- "maxMemBytes": 8598323,
2300
- "avgTotalKeys": 1000,
2301
- "firstSeen": "2024-01-15 10:30:00",
2302
- "lastSeen": "2024-01-15 11:30:00",
2303
- "planInCache": true,
2304
- "planCacheHits": 120,
2305
- "digestText": "SELECT * FROM users WHERE active = ?",
2306
- "plan": "IndexScan(users, idx_active)..."
2307
- }
2308
- ],
2309
- "generatedAt": "2024-01-15T11:30:00.000Z"
2310
- }
2311
- ```
2312
-
2313
- ### Configuration Options
2314
-
2315
- #### Threshold Parameters
2316
-
2317
- | Parameter | Type | Default | Description |
2318
- |-----------|------|---------|-------------|
2319
- | `warnThresholdMs` | `number` | `300` | Latency threshold in milliseconds (secondary) |
2320
- | `memoryThresholdBytes` | `number` | `8 * 1024 * 1024` | **Memory usage threshold in bytes (primary focus)** |
2321
- | `showPlan` | `boolean` | `false` | Whether to include execution plan in logs |
2322
- | `logCache` | `boolean` | `false` | Whether to log cache operations |
2323
-
2324
- **⚠️ Important: OR Logic**
2325
- The monitoring uses **OR logic** - if **either** threshold is exceeded, the query will be logged/returned:
2326
- - Query exceeds `warnThresholdMs` **OR** `memoryThresholdBytes` → Included in results
2327
- - This means you can set different thresholds for different monitoring priorities
2328
- - No need to exceed both thresholds simultaneously
2329
-
2330
- **💡 Pro Tips:**
2331
- - **Memory-only performance monitoring**: Set `warnThresholdMs` to a very high value (e.g., 10000ms) to trigger only on memory usage
2332
- - **Latency-only monitoring**: Set `memoryThresholdBytes` to 16MB (16 * 1024 * 1024) to trigger only on latency
2333
- - **Combined monitoring**: Use both thresholds for comprehensive monitoring
2334
- - **Execution plan analysis**: Set `showPlan: true` to include detailed execution plans in logs (useful for debugging)
2335
- - **Cache debugging**: Set `logCache: true` to log cache operations and debug caching issues
2336
-
2337
- **Memory Threshold Guidelines:**
2338
- - **Conservative**: 4MB (25% of 16MB limit)
2339
- - **Default**: 8MB (50% of 16MB limit)
2340
- - **Aggressive**: 12MB (75% of 16MB limit)
2341
- - **Critical**: 14MB (87.5% of 16MB limit)
2342
-
2343
- #### Available Intervals
2344
-
2345
- - `hour` - **Every hour (Required)** - Statistics are available for approximately 12 hours
2346
-
2347
- > **⚠️ Important**: Due to Forge SQL's statistics retention policy (approximately 12 hours), **only `hour` interval is supported**. Using `day` or `week` intervals will result in incomplete or missing data.
2348
2429
 
2349
2430
  ## Migration Guide
2350
2431
 
@@ -2433,20 +2514,32 @@ const rawUsers = await forgeSQL.execute(
2433
2514
  [true]
2434
2515
  );
2435
2516
 
2517
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
2436
2518
  const cachedRawUsers = await forgeSQL.executeCacheable(
2437
- "SELECT * FROM users WHERE active = ?",
2519
+ "SELECT * FROM `users` WHERE active = ?",
2438
2520
  [true],
2439
2521
  300
2440
2522
  );
2441
2523
 
2442
- // ✅ Raw SQL execution with metadata capture
2524
+ // ✅ Raw SQL execution with metadata capture and performance monitoring
2443
2525
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
2444
- async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
2445
- (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
2446
- console.log(`DB execution time: ${totalDbExecutionTime}ms`);
2447
- console.log(`Response size: ${totalResponseSize} bytes`);
2448
- console.log('Forge metadata:', forgeMetadata);
2526
+ async () => {
2527
+ const users = await forgeSQL.selectFrom(usersTable);
2528
+ const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
2529
+ return { users, orders };
2530
+ },
2531
+ (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
2532
+ const threshold = 500; // ms baseline for this resolver
2533
+
2534
+ if (totalDbExecutionTime > threshold * 1.5) {
2535
+ console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
2536
+ await printQueriesWithPlan(); // Analyze and print query execution plans
2537
+ } else if (totalDbExecutionTime > threshold) {
2538
+ console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
2449
2539
  }
2540
+
2541
+ console.log(`DB response size: ${totalResponseSize} bytes`);
2542
+ }
2450
2543
  );
2451
2544
 
2452
2545
  // ✅ DDL operations for schema modifications