forge-sql-orm 2.1.4 → 2.1.5

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 (45) hide show
  1. package/README.md +91 -5
  2. package/dist/ForgeSQLORM.js +122 -17
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +122 -17
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  7. package/dist/core/ForgeSQLORM.d.ts +23 -0
  8. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  9. package/dist/core/ForgeSQLQueryBuilder.d.ts +36 -5
  10. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  11. package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
  12. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  13. package/dist/utils/cacheContextUtils.d.ts.map +1 -1
  14. package/dist/utils/cacheUtils.d.ts.map +1 -1
  15. package/dist/utils/forgeDriver.d.ts +21 -0
  16. package/dist/utils/forgeDriver.d.ts.map +1 -1
  17. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  18. package/dist/utils/metadataContextUtils.d.ts +11 -0
  19. package/dist/utils/metadataContextUtils.d.ts.map +1 -0
  20. package/dist/utils/sqlUtils.d.ts.map +1 -1
  21. package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
  22. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
  23. package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
  24. package/dist/webtriggers/dropTablesMigrationWebTrigger.d.ts.map +1 -1
  25. package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -1
  26. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +24 -7
  27. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/core/ForgeSQLCrudOperations.ts +3 -0
  30. package/src/core/ForgeSQLORM.ts +119 -51
  31. package/src/core/ForgeSQLQueryBuilder.ts +51 -17
  32. package/src/core/ForgeSQLSelectOperations.ts +2 -0
  33. package/src/lib/drizzle/extensions/additionalActions.ts +2 -0
  34. package/src/utils/cacheContextUtils.ts +4 -2
  35. package/src/utils/cacheUtils.ts +20 -8
  36. package/src/utils/forgeDriver.ts +22 -1
  37. package/src/utils/forgeDriverProxy.ts +2 -0
  38. package/src/utils/metadataContextUtils.ts +24 -0
  39. package/src/utils/sqlUtils.ts +1 -0
  40. package/src/webtriggers/applyMigrationsWebTrigger.ts +5 -2
  41. package/src/webtriggers/clearCacheSchedulerTrigger.ts +1 -0
  42. package/src/webtriggers/dropMigrationWebTrigger.ts +2 -0
  43. package/src/webtriggers/dropTablesMigrationWebTrigger.ts +2 -0
  44. package/src/webtriggers/fetchSchemaWebTrigger.ts +1 -0
  45. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +72 -17
package/README.md CHANGED
@@ -24,6 +24,7 @@
24
24
  - ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
25
25
  - ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
26
26
  - ✅ **Raw SQL Execution**: `execute()` and `executeCacheable()` methods for direct SQL queries with local and global caching
27
+ - ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and Forge SQL metadata
27
28
  - ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
28
29
  - ✅ **Schema migration support**, allowing automatic schema evolution
29
30
  - ✅ **Automatic entity generation** from MySQL/tidb databases
@@ -333,6 +334,16 @@ const cachedRawUsers = await forgeSQL.executeCacheable(
333
334
  300
334
335
  );
335
336
 
337
+ // Raw SQL with execution metadata
338
+ 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);
344
+ }
345
+ );
346
+
336
347
  // Common Table Expressions (CTEs)
337
348
  const userStats = await forgeSQL
338
349
  .with(
@@ -406,6 +417,16 @@ const cachedRawUsers = await forgeSQL.executeCacheable(
406
417
  [true],
407
418
  300
408
419
  );
420
+
421
+ // Raw SQL with execution metadata
422
+ const usersWithMetadata = await forgeSQL.executeWithMetadata(
423
+ async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
424
+ (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
425
+ console.log(`DB execution time: ${totalDbExecutionTime}ms`);
426
+ console.log(`Response size: ${totalResponseSize} bytes`);
427
+ console.log('Forge metadata:', forgeMetadata);
428
+ }
429
+ );
409
430
  ```
410
431
 
411
432
  ## Setting Up Caching with @forge/kvs (Optional)
@@ -777,6 +798,7 @@ const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(
777
798
  | `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
778
799
  | `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
779
800
  | `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
801
+ | `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
780
802
  | `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
781
803
  where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
782
804
 
@@ -1163,6 +1185,17 @@ const users = await forgeSQL
1163
1185
  const users = await forgeSQL
1164
1186
  .executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
1165
1187
 
1188
+ // Using executeWithMetadata() for capturing execution metrics
1189
+ const usersWithMetadata = await forgeSQL
1190
+ .executeWithMetadata(
1191
+ async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
1192
+ (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
1193
+ console.log(`DB execution time: ${totalDbExecutionTime}ms`);
1194
+ console.log(`Response size: ${totalResponseSize} bytes`);
1195
+ console.log('Forge metadata:', forgeMetadata);
1196
+ }
1197
+ );
1198
+
1166
1199
  // Using execute() with complex queries
1167
1200
  const userStats = await forgeSQL
1168
1201
  .execute(`
@@ -1478,6 +1511,16 @@ await forgeSQL.executeWithLocalContext(async () => {
1478
1511
  [true]
1479
1512
  );
1480
1513
 
1514
+ // Raw SQL with execution metadata and local caching
1515
+ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1516
+ async () => await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]),
1517
+ (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
1518
+ console.log(`DB execution time: ${totalDbExecutionTime}ms`);
1519
+ console.log(`Response size: ${totalResponseSize} bytes`);
1520
+ console.log('Forge metadata:', forgeMetadata);
1521
+ }
1522
+ );
1523
+
1481
1524
  // Insert operation - evicts local cache for users table
1482
1525
  await forgeSQL.insert(users).values({ name: 'New User', active: true });
1483
1526
 
@@ -1646,6 +1689,16 @@ const userStats = await forgeSQL
1646
1689
  })
1647
1690
  .from(sql`activeUsers au`)
1648
1691
  .leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
1692
+
1693
+ // Using executeWithMetadata() for capturing execution metrics with caching
1694
+ const usersWithMetadata = await forgeSQL.executeWithMetadata(
1695
+ async () => await forgeSQL.executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300),
1696
+ (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
1697
+ console.log(`DB execution time: ${totalDbExecutionTime}ms`);
1698
+ console.log(`Response size: ${totalResponseSize} bytes`);
1699
+ console.log('Forge metadata:', forgeMetadata);
1700
+ }
1701
+ );
1649
1702
  ```
1650
1703
 
1651
1704
  ### Manual Cache Management
@@ -1721,6 +1774,7 @@ The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
1721
1774
  | Option | Type | Description |
1722
1775
  | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1723
1776
  | `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
1777
+ | `logCache` | `boolean` | Enables logging of cache operations (hits, misses, evictions) in the Atlassian Forge Developer Console. Useful for debugging caching issues. Defaults to `false`. |
1724
1778
  | `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. |
1725
1779
  | `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. |
1726
1780
  | `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"`. |
@@ -2076,19 +2130,38 @@ export const memoryUsageTrigger = () =>
2076
2130
 
2077
2131
  // Conservative memory monitoring: 4MB warning (well below 16MB limit)
2078
2132
  export const conservativeMemoryTrigger = () =>
2079
- topSlowestStatementLastHourTrigger(forgeSQL, 300, 4 * 1024 * 1024);
2133
+ topSlowestStatementLastHourTrigger(forgeSQL, { memoryThresholdBytes: 4 * 1024 * 1024 });
2080
2134
 
2081
2135
  // Aggressive memory monitoring: 12MB warning (75% of 16MB limit)
2082
2136
  export const aggressiveMemoryTrigger = () =>
2083
- topSlowestStatementLastHourTrigger(forgeSQL, 300, 12 * 1024 * 1024);
2137
+ topSlowestStatementLastHourTrigger(forgeSQL, { memoryThresholdBytes: 12 * 1024 * 1024 });
2084
2138
 
2085
2139
  // Memory-only monitoring: Only trigger on memory usage (latency effectively disabled)
2086
2140
  export const memoryOnlyTrigger = () =>
2087
- topSlowestStatementLastHourTrigger(forgeSQL, 10000, 4 * 1024 * 1024);
2141
+ topSlowestStatementLastHourTrigger(forgeSQL, { warnThresholdMs: 10000, memoryThresholdBytes: 4 * 1024 * 1024 });
2088
2142
 
2089
2143
  // Latency-only monitoring: Only trigger on slow queries (memory effectively disabled)
2090
2144
  export const latencyOnlyTrigger = () =>
2091
- topSlowestStatementLastHourTrigger(forgeSQL, 500, 16 * 1024 * 1024);
2145
+ topSlowestStatementLastHourTrigger(forgeSQL, { warnThresholdMs: 500, memoryThresholdBytes: 16 * 1024 * 1024 });
2146
+
2147
+ // With execution plan in logs
2148
+ export const withPlanTrigger = () =>
2149
+ topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true });
2150
+
2151
+ // With cache logging enabled
2152
+ export const withCacheLoggingTrigger = () =>
2153
+ topSlowestStatementLastHourTrigger(forgeSQL, { logCache: true });
2154
+
2155
+ // With both execution plan and cache logging
2156
+ export const withFullLoggingTrigger = () =>
2157
+ topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true, logCache: true });
2158
+
2159
+ // With custom ORM options for debugging
2160
+ const forgeSQL = new ForgeSQL({
2161
+ logRawSqlQuery: true,
2162
+ logCache: true,
2163
+ cacheEntityName: "cache"
2164
+ });
2092
2165
 
2093
2166
 
2094
2167
  #### 3. Configure in manifest.yml
@@ -2196,7 +2269,8 @@ When used as a **web trigger**, the system:
2196
2269
  |-----------|------|---------|-------------|
2197
2270
  | `warnThresholdMs` | `number` | `300` | Latency threshold in milliseconds (secondary) |
2198
2271
  | `memoryThresholdBytes` | `number` | `8 * 1024 * 1024` | **Memory usage threshold in bytes (primary focus)** |
2199
- | `options` | `ForgeSqlOrmOptions` | `undefined` | Optional ORM configuration |
2272
+ | `showPlan` | `boolean` | `false` | Whether to include execution plan in logs |
2273
+ | `logCache` | `boolean` | `false` | Whether to log cache operations |
2200
2274
 
2201
2275
  **⚠️ Important: OR Logic**
2202
2276
  The monitoring uses **OR logic** - if **either** threshold is exceeded, the query will be logged/returned:
@@ -2208,6 +2282,8 @@ The monitoring uses **OR logic** - if **either** threshold is exceeded, the quer
2208
2282
  - **Memory-only monitoring**: Set `warnThresholdMs` to a very high value (e.g., 10000ms) to trigger only on memory usage
2209
2283
  - **Latency-only monitoring**: Set `memoryThresholdBytes` to 16MB (16 * 1024 * 1024) to trigger only on latency
2210
2284
  - **Combined monitoring**: Use both thresholds for comprehensive monitoring
2285
+ - **Execution plan analysis**: Set `showPlan: true` to include detailed execution plans in logs (useful for debugging)
2286
+ - **Cache debugging**: Set `logCache: true` to log cache operations and debug caching issues
2211
2287
 
2212
2288
  **Memory Threshold Guidelines:**
2213
2289
  - **Conservative**: 4MB (25% of 16MB limit)
@@ -2312,6 +2388,16 @@ const cachedRawUsers = await forgeSQL.executeCacheable(
2312
2388
  300
2313
2389
  );
2314
2390
 
2391
+ // ✅ Raw SQL execution with metadata capture
2392
+ const usersWithMetadata = await forgeSQL.executeWithMetadata(
2393
+ async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
2394
+ (totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
2395
+ console.log(`DB execution time: ${totalDbExecutionTime}ms`);
2396
+ console.log(`Response size: ${totalResponseSize} bytes`);
2397
+ console.log('Forge metadata:', forgeMetadata);
2398
+ }
2399
+ );
2400
+
2315
2401
  // ✅ Common Table Expressions (CTEs)
2316
2402
  const userStats = await forgeSQL
2317
2403
  .with(
@@ -410,7 +410,7 @@ async function clearCursorCache(tables, cursor, options) {
410
410
  entityQueryBuilder = entityQueryBuilder.cursor(cursor);
411
411
  }
412
412
  const listResult = await entityQueryBuilder.limit(100).getMany();
413
- if (options.logRawSqlQuery) {
413
+ if (options.logCache) {
414
414
  console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
415
415
  }
416
416
  await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
@@ -431,7 +431,7 @@ async function clearExpirationCursorCache(cursor, options) {
431
431
  entityQueryBuilder = entityQueryBuilder.cursor(cursor);
432
432
  }
433
433
  const listResult = await entityQueryBuilder.limit(100).getMany();
434
- if (options.logRawSqlQuery) {
434
+ if (options.logCache) {
435
435
  console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
436
436
  }
437
437
  await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
@@ -480,7 +480,7 @@ async function clearTablesCache(tables, options) {
480
480
  "clearing cache"
481
481
  );
482
482
  } finally {
483
- if (options.logRawSqlQuery) {
483
+ if (options.logCache) {
484
484
  const duration = luxon.DateTime.now().toSeconds() - startTime.toSeconds();
485
485
  console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
486
486
  }
@@ -499,7 +499,7 @@ async function clearExpiredCache(options) {
499
499
  );
500
500
  } finally {
501
501
  const duration = luxon.DateTime.now().toSeconds() - startTime.toSeconds();
502
- if (options?.logRawSqlQuery) {
502
+ if (options?.logCache) {
503
503
  console.debug(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
504
504
  }
505
505
  }
@@ -514,7 +514,7 @@ async function getFromCache(query, options) {
514
514
  const sqlQuery = query.toSQL();
515
515
  const key = hashKey(sqlQuery);
516
516
  if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
517
- if (options.logRawSqlQuery) {
517
+ if (options.logCache) {
518
518
  console.warn(`Context contains value to clear. Skip getting from cache`);
519
519
  }
520
520
  return void 0;
@@ -522,7 +522,7 @@ async function getFromCache(query, options) {
522
522
  try {
523
523
  const cacheResult = await kvs.kvs.entity(options.cacheEntityName).get(key);
524
524
  if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
525
- if (options.logRawSqlQuery) {
525
+ if (options.logCache) {
526
526
  console.warn(`Get value from cache, cacheKey: ${key}`);
527
527
  }
528
528
  const results = cacheResult[dataName];
@@ -543,7 +543,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
543
543
  const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
544
544
  const sqlQuery = query.toSQL();
545
545
  if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
546
- if (options.logRawSqlQuery) {
546
+ if (options.logCache) {
547
547
  console.warn(`Context contains value to clear. Skip setting from cache`);
548
548
  }
549
549
  return;
@@ -558,7 +558,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
558
558
  },
559
559
  { entityName: options.cacheEntityName }
560
560
  ).execute();
561
- if (options.logRawSqlQuery) {
561
+ if (options.logCache) {
562
562
  console.warn(`Store value to cache, cacheKey: ${key}`);
563
563
  }
564
564
  } catch (error) {
@@ -586,7 +586,7 @@ async function saveQueryLocalCacheQuery(query, rows, options) {
586
586
  sql: sql2.toSQL().sql.toLowerCase(),
587
587
  data: rows
588
588
  };
589
- if (options.logRawSqlQuery) {
589
+ if (options.logCache) {
590
590
  const q = sql2.toSQL();
591
591
  console.debug(
592
592
  `[forge-sql-orm][local-cache][SAVE] Stored result in cache. sql="${q.sql}", params=${JSON.stringify(q.params)}`
@@ -603,7 +603,7 @@ async function getQueryLocalCacheQuery(query, options) {
603
603
  const sql2 = query;
604
604
  const key = hashKey(sql2.toSQL());
605
605
  if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
606
- if (options.logRawSqlQuery) {
606
+ if (options.logCache) {
607
607
  const q = sql2.toSQL();
608
608
  console.debug(
609
609
  `[forge-sql-orm][local-cache][HIT] Returned cached result. sql="${q.sql}", params=${JSON.stringify(q.params)}`
@@ -1034,6 +1034,18 @@ class ForgeSQLSelectOperations {
1034
1034
  return updateQueryResponseResults.rows;
1035
1035
  }
1036
1036
  }
1037
+ const metadataQueryContext = new node_async_hooks.AsyncLocalStorage();
1038
+ async function saveMetaDataInContextContext(metadata) {
1039
+ const context = metadataQueryContext.getStore();
1040
+ if (context && metadata) {
1041
+ context.totalResponseSize += metadata.responseSize;
1042
+ context.totalDbExecutionTime += metadata.dbExecutionTime;
1043
+ context.lastMetadata = metadata;
1044
+ }
1045
+ }
1046
+ async function getLastestMetadata() {
1047
+ return metadataQueryContext.getStore();
1048
+ }
1037
1049
  const forgeDriver = async (query, params, method) => {
1038
1050
  if (method == "execute") {
1039
1051
  const sqlStatement = sql$1.sql.prepare(query);
@@ -1049,6 +1061,7 @@ const forgeDriver = async (query, params, method) => {
1049
1061
  await sqlStatement.bindParams(...params);
1050
1062
  }
1051
1063
  const result = await sqlStatement.execute();
1064
+ await saveMetaDataInContextContext(result.metadata);
1052
1065
  let rows;
1053
1066
  rows = result.rows.map((r) => Object.values(r));
1054
1067
  return { rows };
@@ -1798,6 +1811,7 @@ class ForgeSQLORMImpl {
1798
1811
  try {
1799
1812
  const newOptions = options ?? {
1800
1813
  logRawSqlQuery: false,
1814
+ logCache: false,
1801
1815
  disableOptimisticLocking: false,
1802
1816
  cacheWrapTable: true,
1803
1817
  cacheTTL: 120,
@@ -1823,6 +1837,42 @@ class ForgeSQLORMImpl {
1823
1837
  throw error;
1824
1838
  }
1825
1839
  }
1840
+ /**
1841
+ * Executes a query and provides access to execution metadata.
1842
+ * This method allows you to capture detailed information about query execution
1843
+ * including database execution time, response size, and Forge SQL metadata.
1844
+ *
1845
+ * @template T - The return type of the query
1846
+ * @param query - A function that returns a Promise with the query result
1847
+ * @param onMetadata - Callback function that receives execution metadata
1848
+ * @returns Promise with the query result
1849
+ * @example
1850
+ * ```typescript
1851
+ * const result = await forgeSQL.executeWithMetadata(
1852
+ * async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
1853
+ * (dbTime, responseSize, metadata) => {
1854
+ * console.log(`DB execution time: ${dbTime}ms`);
1855
+ * console.log(`Response size: ${responseSize} bytes`);
1856
+ * console.log('Forge metadata:', metadata);
1857
+ * }
1858
+ * );
1859
+ * ```
1860
+ */
1861
+ async executeWithMetadata(query, onMetadata) {
1862
+ return metadataQueryContext.run({
1863
+ totalDbExecutionTime: 0,
1864
+ totalResponseSize: 0
1865
+ }, async () => {
1866
+ try {
1867
+ return await query();
1868
+ } finally {
1869
+ const metadata = await getLastestMetadata();
1870
+ if (metadata && metadata.lastMetadata) {
1871
+ await onMetadata(metadata.totalDbExecutionTime, metadata.totalResponseSize, metadata.lastMetadata);
1872
+ }
1873
+ }
1874
+ });
1875
+ }
1826
1876
  /**
1827
1877
  * Executes operations within a cache context that collects cache eviction events.
1828
1878
  * All clearCache calls within the context are collected and executed in batch at the end.
@@ -2270,6 +2320,30 @@ class ForgeSQLORM {
2270
2320
  constructor(options) {
2271
2321
  this.ormInstance = ForgeSQLORMImpl.getInstance(options);
2272
2322
  }
2323
+ /**
2324
+ * Executes a query and provides access to execution metadata.
2325
+ * This method allows you to capture detailed information about query execution
2326
+ * including database execution time, response size, and Forge SQL metadata.
2327
+ *
2328
+ * @template T - The return type of the query
2329
+ * @param query - A function that returns a Promise with the query result
2330
+ * @param onMetadata - Callback function that receives execution metadata
2331
+ * @returns Promise with the query result
2332
+ * @example
2333
+ * ```typescript
2334
+ * const result = await forgeSQL.executeWithMetadata(
2335
+ * async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
2336
+ * (dbTime, responseSize, metadata) => {
2337
+ * console.log(`DB execution time: ${dbTime}ms`);
2338
+ * console.log(`Response size: ${responseSize} bytes`);
2339
+ * console.log('Forge metadata:', metadata);
2340
+ * }
2341
+ * );
2342
+ * ```
2343
+ */
2344
+ async executeWithMetadata(query, onMetadata) {
2345
+ return this.ormInstance.executeWithMetadata(query, onMetadata);
2346
+ }
2273
2347
  selectCacheable(fields, cacheTTL) {
2274
2348
  return this.ormInstance.selectCacheable(fields, cacheTTL);
2275
2349
  }
@@ -3218,7 +3292,25 @@ const clearCacheSchedulerTrigger = async (options) => {
3218
3292
  };
3219
3293
  }
3220
3294
  };
3221
- const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, memoryThresholdBytes = 8 * 1024 * 1024) => {
3295
+ const DEFAULT_MEMORY_THRESHOLD = 8 * 1024 * 1024;
3296
+ const DEFAULT_TIMEOUT = 300;
3297
+ const topSlowestStatementLastHourTrigger = async (orm, options) => {
3298
+ if (!orm) {
3299
+ return {
3300
+ statusCode: 500,
3301
+ headers: { "Content-Type": ["application/json"] },
3302
+ body: JSON.stringify({
3303
+ success: false,
3304
+ message: "ORM instance is required",
3305
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3306
+ })
3307
+ };
3308
+ }
3309
+ let newOptions = options ?? {
3310
+ warnThresholdMs: DEFAULT_TIMEOUT,
3311
+ memoryThresholdBytes: DEFAULT_MEMORY_THRESHOLD,
3312
+ showPlan: false
3313
+ };
3222
3314
  const nsToMs = (v) => {
3223
3315
  const n = Number(v);
3224
3316
  return Number.isFinite(n) ? n / 1e6 : NaN;
@@ -3228,6 +3320,17 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3228
3320
  return Number.isFinite(n) ? n / (1024 * 1024) : NaN;
3229
3321
  };
3230
3322
  const jsonSafeStringify = (value) => JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v);
3323
+ function sanitizeSQL(sql2, maxLen = 1e3) {
3324
+ let s = sql2;
3325
+ s = s.replace(/--[^\n\r]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
3326
+ s = s.replace(/'(?:\\'|[^'])*'/g, "?");
3327
+ s = s.replace(/\b-?\d+(?:\.\d+)?\b/g, "?");
3328
+ s = s.replace(/\s+/g, " ").trim();
3329
+ if (s.length > maxLen) {
3330
+ s = s.slice(0, maxLen) + " …[truncated]";
3331
+ }
3332
+ return s;
3333
+ }
3231
3334
  const TOP_N = 1;
3232
3335
  try {
3233
3336
  const summaryHistory = clusterStatementsSummaryHistory;
@@ -3264,7 +3367,8 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3264
3367
  const qHistory = orm.getDrizzleQueryBuilder().select(selectShape(summaryHistory)).from(summaryHistory).where(lastHourFilterHistory);
3265
3368
  const qSummary = orm.getDrizzleQueryBuilder().select(selectShape(summary)).from(summary).where(lastHourFilterSummary);
3266
3369
  const combined = mysqlCore.unionAll(qHistory, qSummary).as("combined");
3267
- const thresholdNs = Math.floor(warnThresholdMs * 1e6);
3370
+ const thresholdNs = Math.floor((newOptions.warnThresholdMs ?? DEFAULT_TIMEOUT) * 1e6);
3371
+ const memoryThresholdBytes = newOptions.memoryThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD;
3268
3372
  const grouped = orm.getDrizzleQueryBuilder().select({
3269
3373
  digest: combined.digest,
3270
3374
  stmtType: combined.stmtType,
@@ -3335,8 +3439,8 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3335
3439
  lastSeen: r.lastSeen,
3336
3440
  planInCache: r.planInCache,
3337
3441
  planCacheHits: r.planCacheHits,
3338
- digestText: r.digestText,
3339
- plan: r.plan
3442
+ digestText: sanitizeSQL(r.digestText),
3443
+ plan: newOptions.showPlan ? r.plan : void 0
3340
3444
  }));
3341
3445
  for (const f of formatted) {
3342
3446
  console.warn(
@@ -3344,7 +3448,7 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
3344
3448
  digest=${f.digest}
3345
3449
  sql=${(f.digestText || "").slice(0, 300)}${f.digestText && f.digestText.length > 300 ? "…" : ""}`
3346
3450
  );
3347
- if (f.plan) {
3451
+ if (newOptions.showPlan && f.plan) {
3348
3452
  console.warn(` full plan:
3349
3453
  ${f.plan}`);
3350
3454
  }
@@ -3357,8 +3461,9 @@ ${f.plan}`);
3357
3461
  success: true,
3358
3462
  window: "last_1h",
3359
3463
  top: TOP_N,
3360
- warnThresholdMs,
3361
- memoryThresholdBytes,
3464
+ warnThresholdMs: newOptions.warnThresholdMs,
3465
+ memoryThresholdBytes: newOptions.memoryThresholdBytes,
3466
+ showPlan: newOptions.showPlan,
3362
3467
  rows: formatted,
3363
3468
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3364
3469
  })