forge-sql-orm 2.1.14 → 2.1.15

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 (31) hide show
  1. package/README.md +290 -20
  2. package/dist/core/ForgeSQLORM.d.ts +16 -7
  3. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  4. package/dist/core/ForgeSQLORM.js +73 -15
  5. package/dist/core/ForgeSQLORM.js.map +1 -1
  6. package/dist/core/ForgeSQLQueryBuilder.d.ts +13 -4
  7. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  9. package/dist/utils/forgeDriver.d.ts +3 -2
  10. package/dist/utils/forgeDriver.d.ts.map +1 -1
  11. package/dist/utils/forgeDriver.js +20 -16
  12. package/dist/utils/forgeDriver.js.map +1 -1
  13. package/dist/utils/metadataContextUtils.d.ts +27 -1
  14. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  15. package/dist/utils/metadataContextUtils.js +215 -10
  16. package/dist/utils/metadataContextUtils.js.map +1 -1
  17. package/dist/webtriggers/index.d.ts +1 -0
  18. package/dist/webtriggers/index.d.ts.map +1 -1
  19. package/dist/webtriggers/index.js +1 -0
  20. package/dist/webtriggers/index.js.map +1 -1
  21. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +60 -0
  22. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
  23. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js +55 -0
  24. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js.map +1 -0
  25. package/package.json +10 -9
  26. package/src/core/ForgeSQLORM.ts +78 -14
  27. package/src/core/ForgeSQLQueryBuilder.ts +13 -3
  28. package/src/utils/forgeDriver.ts +34 -19
  29. package/src/utils/metadataContextUtils.ts +267 -10
  30. package/src/webtriggers/index.ts +1 -0
  31. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +69 -0
@@ -4,6 +4,7 @@ export * from "./fetchSchemaWebTrigger";
4
4
  export * from "./dropTablesMigrationWebTrigger";
5
5
  export * from "./clearCacheSchedulerTrigger";
6
6
  export * from "./slowQuerySchedulerTrigger";
7
+ export * from "./topSlowestStatementLastHourTrigger";
7
8
  export interface TriggerResponse<BODY> {
8
9
  body?: BODY;
9
10
  headers?: Record<string, string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webtriggers/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAE5C,MAAM,WAAW,eAAe,CAAC,IAAI;IACnC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,YAAY,MAAM,EAAE,MAAM,IAAI,KAAG,eAAe,CAAC,IAAI,CAc1F,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webtriggers/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sCAAsC,CAAC;AAErD,MAAM,WAAW,eAAe,CAAC,IAAI;IACnC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,YAAY,MAAM,EAAE,MAAM,IAAI,KAAG,eAAe,CAAC,IAAI,CAc1F,CAAC"}
@@ -21,6 +21,7 @@ __exportStar(require("./fetchSchemaWebTrigger"), exports);
21
21
  __exportStar(require("./dropTablesMigrationWebTrigger"), exports);
22
22
  __exportStar(require("./clearCacheSchedulerTrigger"), exports);
23
23
  __exportStar(require("./slowQuerySchedulerTrigger"), exports);
24
+ __exportStar(require("./topSlowestStatementLastHourTrigger"), exports);
24
25
  const getHttpResponse = (statusCode, body) => {
25
26
  let statusText = "";
26
27
  if (statusCode === 200) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webtriggers/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,4DAA0C;AAC1C,8DAA4C;AAC5C,0DAAwC;AACxC,kEAAgD;AAChD,+DAA6C;AAC7C,8DAA4C;AASrC,MAAM,eAAe,GAAG,CAAO,UAAkB,EAAE,IAAU,EAAyB,EAAE;IAC7F,IAAI,UAAU,GAAG,EAAE,CAAC;IACpB,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;QACvB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;SAAM,CAAC;QACN,UAAU,GAAG,aAAa,CAAC;IAC7B,CAAC;IAED,OAAO;QACL,OAAO,EAAE,EAAE,cAAc,EAAE,CAAC,kBAAkB,CAAC,EAAE;QACjD,UAAU;QACV,UAAU;QACV,IAAI;KACL,CAAC;AACJ,CAAC,CAAC;AAdW,QAAA,eAAe,mBAc1B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webtriggers/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,4DAA0C;AAC1C,8DAA4C;AAC5C,0DAAwC;AACxC,kEAAgD;AAChD,+DAA6C;AAC7C,8DAA4C;AAC5C,uEAAqD;AAS9C,MAAM,eAAe,GAAG,CAAO,UAAkB,EAAE,IAAU,EAAyB,EAAE;IAC7F,IAAI,UAAU,GAAG,EAAE,CAAC;IACpB,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;QACvB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;SAAM,CAAC;QACN,UAAU,GAAG,aAAa,CAAC;IAC7B,CAAC;IAED,OAAO;QACL,OAAO,EAAE,EAAE,cAAc,EAAE,CAAC,kBAAkB,CAAC,EAAE;QACjD,UAAU;QACV,UAAU;QACV,IAAI;KACL,CAAC;AACJ,CAAC,CAAC;AAdW,QAAA,eAAe,mBAc1B"}
@@ -0,0 +1,60 @@
1
+ import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
2
+ import { TriggerResponse } from "./";
3
+ import { OperationType } from "../utils/requestTypeContextUtils";
4
+ export interface TriggerOptions {
5
+ warnThresholdMs?: number;
6
+ memoryThresholdBytes?: number;
7
+ showPlan?: boolean;
8
+ operationType?: OperationType;
9
+ topN?: number;
10
+ hours?: number;
11
+ tables?: "SUMMARY_AND_HISTORY" | "CLUSTER_SUMMARY_AND_HISTORY";
12
+ }
13
+ /**
14
+ * @deprecated This function is deprecated and will be removed in a future version.
15
+ *
16
+ * This function was previously a complex implementation that directly queried
17
+ * CLUSTER_STATEMENTS_SUMMARY tables to analyze query performance. However, this approach
18
+ * had reliability issues with long-running functions where metadata could be evicted
19
+ * before the function completes.
20
+ *
21
+ * The recommended replacement is to use the new observability system with `executeWithMetadata`:
22
+ * - **TopSlowest mode** (default): Deterministic logging of SQL digests executed in resolvers
23
+ * - **SummaryTable mode** (optional): Uses CLUSTER_STATEMENTS_SUMMARY with a short memory window
24
+ * - Automatic fallback mechanisms for long-running functions
25
+ * - More reliable post-mortem diagnostics for Timeout and OOM errors
26
+ *
27
+ * Note: `slowQuerySchedulerTrigger` is a different function that analyzes TiDB's slow query log
28
+ * and is not a direct replacement for this function.
29
+ *
30
+ * For more details on the improvements and migration path, see:
31
+ * https://community.developer.atlassian.com/t/practical-sql-observability-for-forge-apps-with-forge-sql-orm/123456
32
+ *
33
+ * @param orm - ForgeSQL ORM instance
34
+ * @param options - Configuration options (currently passed to slowQuerySchedulerTrigger as a temporary wrapper)
35
+ * @returns Promise<TriggerResponse<string>> - HTTP response with query results or error
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Old usage (deprecated):
40
+ * await topSlowestStatementLastHourTrigger(forgeSQL, { hours: 1 });
41
+ *
42
+ * // New usage (recommended - use executeWithMetadata in your resolvers):
43
+ * await forgeSQL.executeWithMetadata(
44
+ * async () => {
45
+ * // your resolver logic
46
+ * },
47
+ * async (totalDbTime, totalResponseSize, printPlan) => {
48
+ * // custom observability logic
49
+ * if (totalDbTime > 1000) await printPlan();
50
+ * },
51
+ * {
52
+ * mode: "TopSlowest",
53
+ * topQueries: 1,
54
+ * showSlowestPlans: true
55
+ * }
56
+ * );
57
+ * ```
58
+ */
59
+ export declare const topSlowestStatementLastHourTrigger: (orm: ForgeSqlOperation, options?: TriggerOptions) => Promise<TriggerResponse<string>>;
60
+ //# sourceMappingURL=topSlowestStatementLastHourTrigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topSlowestStatementLastHourTrigger.d.ts","sourceRoot":"","sources":["../../src/webtriggers/topSlowestStatementLastHourTrigger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAA6B,eAAe,EAAE,MAAM,IAAI,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,qBAAqB,GAAG,6BAA6B,CAAC;CAChE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,kCAAkC,GAC7C,KAAK,iBAAiB,EACtB,UAAU,cAAc,KACvB,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAKjC,CAAC"}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.topSlowestStatementLastHourTrigger = void 0;
4
+ const _1 = require("./");
5
+ /**
6
+ * @deprecated This function is deprecated and will be removed in a future version.
7
+ *
8
+ * This function was previously a complex implementation that directly queried
9
+ * CLUSTER_STATEMENTS_SUMMARY tables to analyze query performance. However, this approach
10
+ * had reliability issues with long-running functions where metadata could be evicted
11
+ * before the function completes.
12
+ *
13
+ * The recommended replacement is to use the new observability system with `executeWithMetadata`:
14
+ * - **TopSlowest mode** (default): Deterministic logging of SQL digests executed in resolvers
15
+ * - **SummaryTable mode** (optional): Uses CLUSTER_STATEMENTS_SUMMARY with a short memory window
16
+ * - Automatic fallback mechanisms for long-running functions
17
+ * - More reliable post-mortem diagnostics for Timeout and OOM errors
18
+ *
19
+ * Note: `slowQuerySchedulerTrigger` is a different function that analyzes TiDB's slow query log
20
+ * and is not a direct replacement for this function.
21
+ *
22
+ * For more details on the improvements and migration path, see:
23
+ * https://community.developer.atlassian.com/t/practical-sql-observability-for-forge-apps-with-forge-sql-orm/123456
24
+ *
25
+ * @param orm - ForgeSQL ORM instance
26
+ * @param options - Configuration options (currently passed to slowQuerySchedulerTrigger as a temporary wrapper)
27
+ * @returns Promise<TriggerResponse<string>> - HTTP response with query results or error
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Old usage (deprecated):
32
+ * await topSlowestStatementLastHourTrigger(forgeSQL, { hours: 1 });
33
+ *
34
+ * // New usage (recommended - use executeWithMetadata in your resolvers):
35
+ * await forgeSQL.executeWithMetadata(
36
+ * async () => {
37
+ * // your resolver logic
38
+ * },
39
+ * async (totalDbTime, totalResponseSize, printPlan) => {
40
+ * // custom observability logic
41
+ * if (totalDbTime > 1000) await printPlan();
42
+ * },
43
+ * {
44
+ * mode: "TopSlowest",
45
+ * topQueries: 1,
46
+ * showSlowestPlans: true
47
+ * }
48
+ * );
49
+ * ```
50
+ */
51
+ const topSlowestStatementLastHourTrigger = async (orm, options) => {
52
+ return (0, _1.slowQuerySchedulerTrigger)(orm, options ? { timeout: 3000, hours: options.hours ?? 1 } : { timeout: 3000, hours: 1 });
53
+ };
54
+ exports.topSlowestStatementLastHourTrigger = topSlowestStatementLastHourTrigger;
55
+ //# sourceMappingURL=topSlowestStatementLastHourTrigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topSlowestStatementLastHourTrigger.js","sourceRoot":"","sources":["../../src/webtriggers/topSlowestStatementLastHourTrigger.ts"],"names":[],"mappings":";;;AACA,yBAAgE;AAahE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACI,MAAM,kCAAkC,GAAG,KAAK,EACrD,GAAsB,EACtB,OAAwB,EACU,EAAE;IACpC,OAAO,IAAA,4BAAyB,EAC9B,GAAG,EACH,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CACrF,CAAC;AACJ,CAAC,CAAC;AARW,QAAA,kCAAkC,sCAQ7C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-sql-orm",
3
- "version": "2.1.14",
3
+ "version": "2.1.15",
4
4
  "description": "Drizzle ORM integration for Atlassian @forge/sql. Provides a custom driver, schema migration, two levels of caching (local and global via @forge/kvs), optimistic locking, and query analysis.",
5
5
  "main": "dist/index.js",
6
6
  "homepage": "https://github.com/vzakharchenko/forge-sql-orm#readme",
@@ -21,28 +21,29 @@
21
21
  "drizzle-driver",
22
22
  "drizzle-custom-driver",
23
23
  "orm",
24
+ "rovo",
24
25
  "database"
25
26
  ],
26
27
  "devDependencies": {
27
28
  "@eslint/js": "^9.39.1",
28
29
  "@types/luxon": "^3.7.1",
29
30
  "@types/node": "^24.10.1",
30
- "@typescript-eslint/eslint-plugin": "^8.47.0",
31
- "@typescript-eslint/parser": "^8.47.0",
32
- "@vitest/coverage-v8": "^4.0.13",
33
- "@vitest/ui": "^4.0.13",
31
+ "@typescript-eslint/eslint-plugin": "^8.48.0",
32
+ "@typescript-eslint/parser": "^8.48.0",
33
+ "@vitest/coverage-v8": "^4.0.14",
34
+ "@vitest/ui": "^4.0.14",
34
35
  "eslint": "^9.39.1",
35
36
  "eslint-config-prettier": "^10.1.8",
36
37
  "eslint-plugin-import": "^2.32.0",
37
38
  "eslint-plugin-vitest": "^0.5.4",
38
39
  "husky": "^9.1.7",
39
- "knip": "^5.70.1",
40
+ "knip": "^5.70.2",
40
41
  "patch-package": "^8.0.1",
41
- "prettier": "^3.6.2",
42
+ "prettier": "^3.7.2",
42
43
  "ts-node": "^10.9.2",
43
44
  "typescript": "^5.9.3",
44
45
  "uuid": "^13.0.0",
45
- "vitest": "^4.0.13"
46
+ "vitest": "^4.0.14"
46
47
  },
47
48
  "license": "MIT",
48
49
  "author": "Vasyl Zakharchenko",
@@ -70,7 +71,7 @@
70
71
  "README.md"
71
72
  ],
72
73
  "peerDependencies": {
73
- "@forge/sql": "^3.0.10",
74
+ "@forge/sql": "^3.0.12",
74
75
  "drizzle-orm": "^0.44.7"
75
76
  },
76
77
  "optionalDependencies": {
@@ -39,7 +39,11 @@ import { cacheApplicationContext, localCacheApplicationContext } from "../utils/
39
39
  import { clearTablesCache } from "../utils/cacheUtils";
40
40
  import { SQLWrapper } from "drizzle-orm/sql/sql";
41
41
  import { WithSubquery } from "drizzle-orm/subquery";
42
- import { getLastestMetadata, metadataQueryContext } from "../utils/metadataContextUtils";
42
+ import {
43
+ getLastestMetadata,
44
+ metadataQueryContext,
45
+ MetadataQueryOptions,
46
+ } from "../utils/metadataContextUtils";
43
47
  import { operationTypeQueryContext } from "../utils/requestTypeContextUtils";
44
48
  import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
45
49
  import { Rovo } from "./Rovo";
@@ -124,7 +128,15 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
124
128
  * @param onMetadata - Callback function that receives aggregated execution metadata
125
129
  * @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
126
130
  * @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
127
- * @param onMetadata.printQueries - Function to analyze and print query execution plans from CLUSTER_STATEMENTS_SUMMARY
131
+ * @param onMetadata.printQueriesWithPlan - Function to analyze and print query execution plans. Supports two modes:
132
+ * - TopSlowest: Prints execution plans for the slowest queries from the current resolver (default)
133
+ * - SummaryTable: Uses CLUSTER_STATEMENTS_SUMMARY if within time window
134
+ * @param options - Optional configuration for query plan printing behavior
135
+ * @param options.mode - Query plan printing mode: 'TopSlowest' (default) or 'SummaryTable'
136
+ * @param options.summaryTableWindowTime - Time window in milliseconds for summary table queries (default: 15000ms). Only used when mode is 'SummaryTable'
137
+ * @param options.topQueries - Number of top slowest queries to analyze when mode is 'TopSlowest' (default: 1)
138
+ * @param options.showSlowestPlans - Whether to show execution plans for slowest queries in TopSlowest mode (default: true)
139
+ * @param options.normalizeQuery - Whether to normalize SQL queries by replacing parameter values with '?' placeholders (default: true). Set to false to disable normalization if it causes issues
128
140
  * @returns Promise with the query result
129
141
  *
130
142
  * @example
@@ -136,12 +148,12 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
136
148
  * const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
137
149
  * return { users, orders };
138
150
  * },
139
- * (totalDbExecutionTime, totalResponseSize, printQueries) => {
151
+ * (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
140
152
  * const threshold = 500; // ms baseline for this resolver
141
153
  *
142
154
  * if (totalDbExecutionTime > threshold * 1.5) {
143
155
  * console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
144
- * await printQueries(); // Analyze and print query execution plans
156
+ * await printQueriesWithPlan(); // Analyze and print query execution plans
145
157
  * } else if (totalDbExecutionTime > threshold) {
146
158
  * console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
147
159
  * }
@@ -164,12 +176,12 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
164
176
  * .where(eq(demoOrders.userId, demoUsers.id));
165
177
  * return { users, orders };
166
178
  * },
167
- * async (totalDbExecutionTime, totalResponseSize, printQueries) => {
179
+ * async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
168
180
  * const threshold = 500; // ms baseline for this resolver
169
181
  *
170
182
  * if (totalDbExecutionTime > threshold * 1.5) {
171
183
  * console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
172
- * await printQueries(); // Optionally log or capture diagnostics for further analysis
184
+ * await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
173
185
  * } else if (totalDbExecutionTime > threshold) {
174
186
  * console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
175
187
  * }
@@ -185,7 +197,47 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
185
197
  * });
186
198
  * ```
187
199
  *
188
- * @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueries()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
200
+ * @example
201
+ * ```typescript
202
+ * // Using TopSlowest mode with custom topQueries
203
+ * const result = await forgeSQL.executeWithMetadata(
204
+ * async () => {
205
+ * const users = await forgeSQL.selectFrom(usersTable);
206
+ * return users;
207
+ * },
208
+ * async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
209
+ * if (totalDbExecutionTime > 1000) {
210
+ * await printQueriesWithPlan(); // Will print top 3 slowest queries
211
+ * }
212
+ * },
213
+ * {
214
+ * mode: 'TopSlowest', // Print top slowest queries (default)
215
+ * topQueries: 3, // Print top 3 slowest queries
216
+ * }
217
+ * );
218
+ * ```
219
+ *
220
+ * @example
221
+ * ```typescript
222
+ * // Using SummaryTable mode for query analysis
223
+ * const result = await forgeSQL.executeWithMetadata(
224
+ * async () => {
225
+ * const users = await forgeSQL.selectFrom(usersTable);
226
+ * return users;
227
+ * },
228
+ * async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
229
+ * if (totalDbExecutionTime > 1000) {
230
+ * await printQueriesWithPlan(); // Will use CLUSTER_STATEMENTS_SUMMARY if within time window
231
+ * }
232
+ * },
233
+ * {
234
+ * mode: 'SummaryTable', // Use summary tables mode
235
+ * summaryTableWindowTime: 10000, // 10 second window
236
+ * }
237
+ * );
238
+ * ```
239
+ *
240
+ * @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueriesWithPlan()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
189
241
  */
190
242
  async executeWithMetadata<T>(
191
243
  query: () => Promise<T>,
@@ -194,6 +246,7 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
194
246
  totalResponseSize: number,
195
247
  printQueriesWithPlan: () => Promise<void>,
196
248
  ) => Promise<void> | void,
249
+ options?: MetadataQueryOptions,
197
250
  ): Promise<T> {
198
251
  return metadataQueryContext.run(
199
252
  {
@@ -204,6 +257,8 @@ class ForgeSQLORMImpl implements ForgeSqlOperation {
204
257
  printQueriesWithPlan: async () => {
205
258
  return;
206
259
  },
260
+ options: options,
261
+ statistics: [],
207
262
  },
208
263
  async () => {
209
264
  const result = await query();
@@ -886,7 +941,15 @@ class ForgeSQLORM implements ForgeSqlOperation {
886
941
  * @param onMetadata - Callback function that receives aggregated execution metadata
887
942
  * @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
888
943
  * @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
889
- * @param onMetadata.printQueries - Function to analyze and print query execution plans from CLUSTER_STATEMENTS_SUMMARY
944
+ * @param onMetadata.printQueriesWithPlan - Function to analyze and print query execution plans. Supports two modes:
945
+ * - TopSlowest: Prints execution plans for the slowest queries from the current resolver (default)
946
+ * - SummaryTable: Uses CLUSTER_STATEMENTS_SUMMARY if within time window
947
+ * @param options - Optional configuration for query plan printing behavior
948
+ * @param options.mode - Query plan printing mode: 'TopSlowest' (default) or 'SummaryTable'
949
+ * @param options.summaryTableWindowTime - Time window in milliseconds for summary table queries (default: 15000ms). Only used when mode is 'SummaryTable'
950
+ * @param options.topQueries - Number of top slowest queries to analyze when mode is 'TopSlowest' (default: 1)
951
+ * @param options.showSlowestPlans - Whether to show execution plans for slowest queries in TopSlowest mode (default: true)
952
+ * @param options.normalizeQuery - Whether to normalize SQL queries by replacing parameter values with '?' placeholders (default: true). Set to false to disable normalization if it causes issues
890
953
  * @returns Promise with the query result
891
954
  *
892
955
  * @example
@@ -898,12 +961,12 @@ class ForgeSQLORM implements ForgeSqlOperation {
898
961
  * const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
899
962
  * return { users, orders };
900
963
  * },
901
- * (totalDbExecutionTime, totalResponseSize, printQueries) => {
964
+ * (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
902
965
  * const threshold = 500; // ms baseline for this resolver
903
966
  *
904
967
  * if (totalDbExecutionTime > threshold * 1.5) {
905
968
  * console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
906
- * await printQueries(); // Analyze and print query execution plans
969
+ * await printQueriesWithPlan(); // Analyze and print query execution plans
907
970
  * } else if (totalDbExecutionTime > threshold) {
908
971
  * console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
909
972
  * }
@@ -926,12 +989,12 @@ class ForgeSQLORM implements ForgeSqlOperation {
926
989
  * .where(eq(demoOrders.userId, demoUsers.id));
927
990
  * return { users, orders };
928
991
  * },
929
- * async (totalDbExecutionTime, totalResponseSize, printQueries) => {
992
+ * async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
930
993
  * const threshold = 500; // ms baseline for this resolver
931
994
  *
932
995
  * if (totalDbExecutionTime > threshold * 1.5) {
933
996
  * console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
934
- * await printQueries(); // Optionally log or capture diagnostics for further analysis
997
+ * await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
935
998
  * } else if (totalDbExecutionTime > threshold) {
936
999
  * console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
937
1000
  * }
@@ -947,7 +1010,7 @@ class ForgeSQLORM implements ForgeSqlOperation {
947
1010
  * });
948
1011
  * ```
949
1012
  *
950
- * @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueries()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
1013
+ * @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueriesWithPlan()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
951
1014
  */
952
1015
  async executeWithMetadata<T>(
953
1016
  query: () => Promise<T>,
@@ -956,8 +1019,9 @@ class ForgeSQLORM implements ForgeSqlOperation {
956
1019
  totalResponseSize: number,
957
1020
  printQueriesWithPlan: () => Promise<void>,
958
1021
  ) => Promise<void> | void,
1022
+ options?: MetadataQueryOptions,
959
1023
  ): Promise<T> {
960
- return this.ormInstance.executeWithMetadata(query, onMetadata);
1024
+ return this.ormInstance.executeWithMetadata(query, onMetadata, options);
961
1025
  }
962
1026
 
963
1027
  selectCacheable<TSelection extends SelectedFields>(
@@ -57,6 +57,7 @@ import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
57
57
  import type { WithBuilder } from "drizzle-orm/mysql-core/subquery";
58
58
  import { WithSubquery } from "drizzle-orm/subquery";
59
59
  import { MySqlColumn } from "drizzle-orm/mysql-core";
60
+ import { MetadataQueryOptions } from "../utils/metadataContextUtils";
60
61
 
61
62
  /**
62
63
  * Core interface for ForgeSQL operations.
@@ -585,7 +586,15 @@ export interface QueryBuilderForgeSql {
585
586
  * @param onMetadata - Callback function that receives aggregated execution metadata
586
587
  * @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
587
588
  * @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
588
- * @param onMetadata.printQueries - Function to analyze and print query execution plans from CLUSTER_STATEMENTS_SUMMARY
589
+ * @param onMetadata.printQueriesWithPlan - Function to analyze and print query execution plans. Supports two modes:
590
+ * - TopSlowest: Prints execution plans for the slowest queries from the current resolver (default)
591
+ * - SummaryTable: Uses CLUSTER_STATEMENTS_SUMMARY if within time window
592
+ * @param options - Optional configuration for query plan printing behavior
593
+ * @param options.mode - Query plan printing mode: 'TopSlowest' (default) or 'SummaryTable'
594
+ * @param options.summaryTableWindowTime - Time window in milliseconds for summary table queries (default: 15000ms). Only used when mode is 'SummaryTable'
595
+ * @param options.topQueries - Number of top slowest queries to analyze when mode is 'TopSlowest' (default: 1)
596
+ * @param options.showSlowestPlans - Whether to show execution plans for slowest queries in TopSlowest mode (default: true)
597
+ * @param options.normalizeQuery - Whether to normalize SQL queries by replacing parameter values with '?' placeholders (default: true). Set to false to disable normalization if it causes issues
589
598
  * @returns Promise with the query result
590
599
  *
591
600
  * @example
@@ -597,12 +606,12 @@ export interface QueryBuilderForgeSql {
597
606
  * const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
598
607
  * return { users, orders };
599
608
  * },
600
- * (totalDbExecutionTime, totalResponseSize, printQueries) => {
609
+ * (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
601
610
  * const threshold = 500; // ms baseline for this resolver
602
611
  *
603
612
  * if (totalDbExecutionTime > threshold * 1.5) {
604
613
  * console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
605
- * await printQueries(); // Analyze and print query execution plans
614
+ * await printQueriesWithPlan(); // Analyze and print query execution plans
606
615
  * } else if (totalDbExecutionTime > threshold) {
607
616
  * console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
608
617
  * }
@@ -655,6 +664,7 @@ export interface QueryBuilderForgeSql {
655
664
  totalResponseSize: number,
656
665
  printQueriesWithPlan: () => Promise<void>,
657
666
  ) => Promise<void> | void,
667
+ options?: MetadataQueryOptions,
658
668
  ): Promise<T>;
659
669
  /**
660
670
  * Executes a raw SQL query with local cache support.
@@ -77,14 +77,22 @@ function inlineParams(sql: string, params: unknown[]): string {
77
77
  }
78
78
 
79
79
  /**
80
- * Processes DDL query results and saves metadata.
80
+ * Processes DDL query results and saves metadata to the execution context.
81
81
  *
82
+ * @param query - The SQL query string
83
+ * @param params - Query parameters
84
+ * @param method - Execution method ("all" or "execute")
82
85
  * @param result - The DDL query result
83
86
  * @returns Processed result for Drizzle ORM
84
87
  */
85
- async function processDDLResult(method: QueryMethod, result: any): Promise<ForgeDriverResult> {
88
+ async function processDDLResult(
89
+ query: string,
90
+ params: unknown[],
91
+ method: QueryMethod,
92
+ result: any,
93
+ ): Promise<ForgeDriverResult> {
86
94
  if (result.metadata) {
87
- await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
95
+ await saveMetaDataToContext(query, params, result.metadata as ForgeSQLMetadata);
88
96
  }
89
97
 
90
98
  if (!result?.rows) {
@@ -109,13 +117,16 @@ async function processDDLResult(method: QueryMethod, result: any): Promise<Forge
109
117
  }
110
118
 
111
119
  /**
112
- * Processes execute method results (UPDATE, INSERT, DELETE).
120
+ * Processes execute method results (UPDATE, INSERT, DELETE) and saves metadata to the execution context.
113
121
  *
114
- * @param query - The SQL query
115
- * @param params - Query parameters
122
+ * @param query - The SQL query string
123
+ * @param params - Query parameters (may be undefined)
116
124
  * @returns Processed result for Drizzle ORM
117
125
  */
118
- async function processExecuteMethod(query: string, params: unknown[]): Promise<ForgeDriverResult> {
126
+ async function processExecuteMethod(
127
+ query: string,
128
+ params: unknown[] | undefined,
129
+ ): Promise<ForgeDriverResult> {
119
130
  const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
120
131
 
121
132
  if (params) {
@@ -123,7 +134,7 @@ async function processExecuteMethod(query: string, params: unknown[]): Promise<F
123
134
  }
124
135
 
125
136
  const result = await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs);
126
- await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
137
+ await saveMetaDataToContext(query, params ?? [], result.metadata as ForgeSQLMetadata);
127
138
  if (!result.rows) {
128
139
  return { rows: [[]] };
129
140
  }
@@ -132,13 +143,16 @@ async function processExecuteMethod(query: string, params: unknown[]): Promise<F
132
143
  }
133
144
 
134
145
  /**
135
- * Processes all method results (SELECT queries).
146
+ * Processes all method results (SELECT queries) and saves metadata to the execution context.
136
147
  *
137
- * @param query - The SQL query
138
- * @param params - Query parameters
148
+ * @param query - The SQL query string
149
+ * @param params - Query parameters (may be undefined)
139
150
  * @returns Processed result for Drizzle ORM
140
151
  */
141
- async function processAllMethod(query: string, params: unknown[]): Promise<ForgeDriverResult> {
152
+ async function processAllMethod(
153
+ query: string,
154
+ params: unknown[] | undefined,
155
+ ): Promise<ForgeDriverResult> {
142
156
  const sqlStatement = await sql.prepare<unknown>(query);
143
157
 
144
158
  if (params) {
@@ -146,7 +160,7 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
146
160
  }
147
161
 
148
162
  const result = await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs);
149
- await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
163
+ await saveMetaDataToContext(query, params ?? [], result.metadata as ForgeSQLMetadata);
150
164
 
151
165
  if (!result.rows) {
152
166
  return { rows: [] };
@@ -160,9 +174,10 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
160
174
  /**
161
175
  * Main Forge SQL driver function for Drizzle ORM integration.
162
176
  * Handles DDL operations, execute operations (UPDATE/INSERT/DELETE), and select operations.
177
+ * Automatically saves query execution metadata to the context for performance monitoring.
163
178
  *
164
179
  * @param query - The SQL query to execute
165
- * @param params - Query parameters
180
+ * @param params - Query parameters (may be undefined or empty array)
166
181
  * @param method - Execution method ("all" for SELECT, "execute" for UPDATE/INSERT/DELETE)
167
182
  * @returns Promise with query results compatible with Drizzle ORM
168
183
  *
@@ -182,25 +197,25 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
182
197
  */
183
198
  export const forgeDriver = async (
184
199
  query: string,
185
- params: unknown[],
200
+ params: unknown[] | undefined,
186
201
  method: QueryMethod,
187
202
  ): Promise<ForgeDriverResult> => {
188
203
  const operationType = await getOperationType();
189
204
  // Handle DDL operations
190
205
  if (operationType === "DDL") {
191
206
  const result = await withTimeout(
192
- sql.executeDDL(inlineParams(query, params)),
207
+ sql.executeDDL(inlineParams(query, params ?? [])),
193
208
  timeoutMessage,
194
209
  timeoutMs,
195
210
  );
196
- return await processDDLResult(method, result);
211
+ return await processDDLResult(query, params ?? [], method, result);
197
212
  }
198
213
 
199
214
  // Handle execute method (UPDATE, INSERT, DELETE)
200
215
  if (method === "execute") {
201
- return await processExecuteMethod(query, params ?? []);
216
+ return await processExecuteMethod(query, params);
202
217
  }
203
218
 
204
219
  // Handle all method (SELECT)
205
- return await processAllMethod(query, params ?? []);
220
+ return await processAllMethod(query, params);
206
221
  };