forge-sql-orm 2.1.11 → 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.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  - ✅ **Custom Drizzle Driver** for direct integration with @forge/sql
19
19
  - ✅ **Local Cache System (Level 1)** for in-memory query optimization within single resolver invocation scope
20
20
  - ✅ **Global Cache System (Level 2)** with cross-invocation caching, automatic cache invalidation and context-aware operations (using [@forge/kvs](https://developer.atlassian.com/platform/forge/storage-reference/storage-api-custom-entities/) )
21
- - ✅ **Performance Monitoring**: Query execution metrics and analysis capabilities with automatic error analysis for timeout and OOM errors
21
+ - ✅ **Performance Monitoring**: Query execution metrics and analysis capabilities with automatic error analysis for timeout and OOM errors, plus scheduled slow query monitoring with execution plans
22
22
  - ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
23
23
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
24
24
  - ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
@@ -64,7 +64,8 @@
64
64
  ### 🔒 Advanced Features
65
65
  - [Optimistic Locking](#optimistic-locking)
66
66
  - [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
67
- - [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
68
69
  - [Date and Time Types](#date-and-time-types)
69
70
 
70
71
  ### 🛠️ Development Tools
@@ -294,10 +295,8 @@ resolver.define("fetch", async (req: Request) => {
294
295
  console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
295
296
  await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
296
297
  } else if (totalDbExecutionTime > threshold) {
297
- console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
298
+ console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
298
299
  }
299
-
300
- console.log(`DB response size: ${totalResponseSize} bytes`);
301
300
  }
302
301
  );
303
302
  } catch (e) {
@@ -360,8 +359,9 @@ const rawUsers = await forgeSQL.execute(
360
359
  );
361
360
 
362
361
  // Raw SQL with caching
362
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
363
363
  const cachedRawUsers = await forgeSQL.executeCacheable(
364
- "SELECT * FROM users WHERE active = ?",
364
+ "SELECT * FROM `users` WHERE active = ?",
365
365
  [true],
366
366
  300
367
367
  );
@@ -481,8 +481,9 @@ const rawUsers = await forgeSQL.execute(
481
481
  );
482
482
 
483
483
  // Raw SQL with caching
484
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
484
485
  const cachedRawUsers = await forgeSQL.executeCacheable(
485
- "SELECT * FROM users WHERE active = ?",
486
+ "SELECT * FROM `users` WHERE active = ?",
486
487
  [true],
487
488
  300
488
489
  );
@@ -617,6 +618,9 @@ Please review the [official @forge/kvs quotas and limits](https://developer.atla
617
618
  - Monitor cache usage to stay within quotas
618
619
  - Use appropriate TTL values
619
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
+
620
624
  ### Step 1: Install Dependencies
621
625
 
622
626
  ```bash
@@ -1136,8 +1140,10 @@ const user = await forgeSQL
1136
1140
  .execute("SELECT * FROM users WHERE id = ?", [1]);
1137
1141
 
1138
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 = ?)
1139
1145
  const user = await forgeSQL
1140
- .executeCacheable("SELECT * FROM users WHERE id = ?", [1], 300);
1146
+ .executeCacheable("SELECT * FROM `users` WHERE id = ?", [1], 300);
1141
1147
 
1142
1148
  // Using forgeSQL.getDrizzleQueryBuilder()
1143
1149
  const user = await forgeSQL
@@ -1266,8 +1272,10 @@ const users = await forgeSQL
1266
1272
  .execute("SELECT * FROM users WHERE active = ?", [true]);
1267
1273
 
1268
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 = ?)
1269
1277
  const users = await forgeSQL
1270
- .executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
1278
+ .executeCacheable("SELECT * FROM `users` WHERE active = ?", [true], 300);
1271
1279
 
1272
1280
  // Using executeWithMetadata() for capturing execution metrics and performance monitoring
1273
1281
  const usersWithMetadata = await forgeSQL.executeWithMetadata(
@@ -1760,8 +1768,9 @@ await forgeSQL.executeWithLocalContext(async () => {
1760
1768
  .where(eq(users.active, true));
1761
1769
 
1762
1770
  // Raw SQL with multi-level caching
1771
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1763
1772
  const rawUsers = await forgeSQL.executeCacheable(
1764
- "SELECT id, name FROM users WHERE active = ?",
1773
+ "SELECT id, name FROM `users` WHERE active = ?",
1765
1774
  [true],
1766
1775
  300 // TTL in seconds
1767
1776
  );
@@ -1810,8 +1819,9 @@ const usersDistinct = await forgeSQL.selectDistinctCacheableFrom(Users)
1810
1819
  .where(eq(Users.active, true));
1811
1820
 
1812
1821
  // Raw SQL with local and global caching
1822
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
1813
1823
  const rawUsers = await forgeSQL.executeCacheable(
1814
- "SELECT * FROM users WHERE active = ?",
1824
+ "SELECT * FROM `users` WHERE active = ?",
1815
1825
  [true],
1816
1826
  300 // TTL in seconds
1817
1827
  );
@@ -2122,6 +2132,34 @@ Configure in `manifest.yml`:
2122
2132
  - `hour` - Every hour
2123
2133
  - `day` - Every day
2124
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
+
2125
2163
  ### Important Notes
2126
2164
 
2127
2165
  **Security Considerations**:
@@ -2210,6 +2248,112 @@ The error analysis mechanism:
2210
2248
 
2211
2249
  > **💡 Tip**: The automatic error analysis only triggers for timeout and OOM errors. Other errors are logged normally without plan analysis.
2212
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.
2356
+
2213
2357
  ### Available Analysis Tools
2214
2358
 
2215
2359
  ```typescript
@@ -2370,8 +2514,9 @@ const rawUsers = await forgeSQL.execute(
2370
2514
  [true]
2371
2515
  );
2372
2516
 
2517
+ // ⚠️ IMPORTANT: When using executeCacheable(), all table names must be wrapped with backticks (`)
2373
2518
  const cachedRawUsers = await forgeSQL.executeCacheable(
2374
- "SELECT * FROM users WHERE active = ?",
2519
+ "SELECT * FROM `users` WHERE active = ?",
2375
2520
  [true],
2376
2521
  300
2377
2522
  );
@@ -823,7 +823,14 @@ async function printQueriesWithPlan(forgeSQLORM, timeDiffMs, timeout) {
823
823
  drizzleOrm.and(
824
824
  drizzleOrm.isNotNull(statementsTable.digest),
825
825
  drizzleOrm.not(drizzleOrm.ilike(statementsTable.digestText, "%information_schema%")),
826
- drizzleOrm.notInArray(statementsTable.stmtType, ["Use", "Set", "Show", "Commit", "Rollback", "Begin"]),
826
+ drizzleOrm.notInArray(statementsTable.stmtType, [
827
+ "Use",
828
+ "Set",
829
+ "Show",
830
+ "Commit",
831
+ "Rollback",
832
+ "Begin"
833
+ ]),
827
834
  drizzleOrm.gte(
828
835
  statementsTable.lastSeen,
829
836
  drizzleOrm.sql`DATE_SUB
@@ -885,9 +892,7 @@ async function slowQueryPerHours(forgeSQLORM, hours, timeout) {
885
892
  const message = `Found SlowQuery SQL: ${result.query} | Memory: ${memMaxMB.toFixed(2)} MB | Time: ${result.queryTime} ms
886
893
  Plan:${result.plan}`;
887
894
  response.push(message);
888
- console.warn(
889
- message
890
- );
895
+ console.warn(message);
891
896
  });
892
897
  return response;
893
898
  } catch (error) {
@@ -895,16 +900,16 @@ async function slowQueryPerHours(forgeSQLORM, hours, timeout) {
895
900
  `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
896
901
  error
897
902
  );
898
- return [`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`];
903
+ return [
904
+ `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`
905
+ ];
899
906
  }
900
907
  }
901
908
  async function withTimeout(promise, message, timeoutMs2) {
902
909
  let timeoutId;
903
910
  const timeoutPromise = new Promise((_, reject) => {
904
911
  timeoutId = setTimeout(() => {
905
- reject(
906
- new Error(message)
907
- );
912
+ reject(new Error(message));
908
913
  }, timeoutMs2);
909
914
  });
910
915
  try {
@@ -936,6 +941,17 @@ function nowPlusSeconds(secondsToAdd) {
936
941
  const dt = luxon.DateTime.now().plus({ seconds: secondsToAdd });
937
942
  return Math.floor(dt.toSeconds());
938
943
  }
944
+ function extractBacktickedValues(sql2) {
945
+ const regex = /`([^`]+)`/g;
946
+ const matches = /* @__PURE__ */ new Set();
947
+ let match;
948
+ while ((match = regex.exec(sql2.toLowerCase())) !== null) {
949
+ if (!match[1].startsWith("a_")) {
950
+ matches.add(`\`${match[1]}\``);
951
+ }
952
+ }
953
+ return Array.from(matches).sort().join(",");
954
+ }
939
955
  function hashKey(query) {
940
956
  const h = crypto__namespace.createHash("sha256");
941
957
  h.update(query.sql.toLowerCase());
@@ -1079,7 +1095,7 @@ async function getFromCache(query, options) {
1079
1095
  }
1080
1096
  try {
1081
1097
  const cacheResult = await kvs.kvs.entity(options.cacheEntityName).get(key);
1082
- if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
1098
+ if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && extractBacktickedValues(sqlQuery.sql) === cacheResult[entityQueryName]) {
1083
1099
  if (options.logCache) {
1084
1100
  console.warn(`Get value from cache, cacheKey: ${key}`);
1085
1101
  }
@@ -1110,7 +1126,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
1110
1126
  await kvs.kvs.transact().set(
1111
1127
  key,
1112
1128
  {
1113
- [entityQueryName]: sqlQuery.sql.toLowerCase(),
1129
+ [entityQueryName]: extractBacktickedValues(sqlQuery.sql),
1114
1130
  [expirationName]: nowPlusSeconds(cacheTtl),
1115
1131
  [dataName]: JSON.stringify(results)
1116
1132
  },
@@ -1613,10 +1629,7 @@ async function saveMetaDataToContext(metadata) {
1613
1629
  if (process.env.NODE_ENV !== "test") {
1614
1630
  await new Promise((r) => setTimeout(r, 200));
1615
1631
  }
1616
- await printQueriesWithPlan(
1617
- context.forgeSQLORM,
1618
- Date.now() - context.beginTime.getTime()
1619
- );
1632
+ await printQueriesWithPlan(context.forgeSQLORM, Date.now() - context.beginTime.getTime());
1620
1633
  };
1621
1634
  if (metadata) {
1622
1635
  context.totalResponseSize += metadata.responseSize;
@@ -1683,7 +1696,11 @@ async function processAllMethod(query, params) {
1683
1696
  if (params) {
1684
1697
  await sqlStatement.bindParams(...params);
1685
1698
  }
1686
- const result = await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs);
1699
+ const result = await withTimeout(
1700
+ sqlStatement.execute(),
1701
+ timeoutMessage,
1702
+ timeoutMs
1703
+ );
1687
1704
  await saveMetaDataToContext(result.metadata);
1688
1705
  if (!result.rows) {
1689
1706
  return { rows: [] };
@@ -1694,7 +1711,11 @@ async function processAllMethod(query, params) {
1694
1711
  const forgeDriver = async (query, params, method) => {
1695
1712
  const operationType = await getOperationType();
1696
1713
  if (operationType === "DDL") {
1697
- const result = await withTimeout(sql.sql.executeDDL(inlineParams(query, params)), timeoutMessage, timeoutMs);
1714
+ const result = await withTimeout(
1715
+ sql.sql.executeDDL(inlineParams(query, params)),
1716
+ timeoutMessage,
1717
+ timeoutMs
1718
+ );
1698
1719
  return await processDDLResult(method, result);
1699
1720
  }
1700
1721
  if (method === "execute") {
@@ -3833,7 +3854,12 @@ const clearCacheSchedulerTrigger = async (options) => {
3833
3854
  };
3834
3855
  async function slowQuerySchedulerTrigger(forgeSQLORM, options) {
3835
3856
  try {
3836
- return getHttpResponse(200, JSON.stringify(await slowQueryPerHours(forgeSQLORM, options?.hours ?? 1, options?.timeout ?? 3e3)));
3857
+ return getHttpResponse(
3858
+ 200,
3859
+ JSON.stringify(
3860
+ await slowQueryPerHours(forgeSQLORM, options?.hours ?? 1, options?.timeout ?? 3e3)
3861
+ )
3862
+ );
3837
3863
  } catch (error) {
3838
3864
  const errorMessage = error?.debug?.sqlMessage ?? error?.debug?.message ?? error.message ?? "Unknown error occurred";
3839
3865
  console.error(errorMessage);