forge-sql-orm 2.1.14 → 2.1.16

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 (65) hide show
  1. package/README.md +294 -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 +15 -7
  7. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  9. package/dist/core/ForgeSQLSelectOperations.d.ts +2 -1
  10. package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
  11. package/dist/core/ForgeSQLSelectOperations.js.map +1 -1
  12. package/dist/core/Rovo.d.ts +40 -0
  13. package/dist/core/Rovo.d.ts.map +1 -1
  14. package/dist/core/Rovo.js +164 -138
  15. package/dist/core/Rovo.js.map +1 -1
  16. package/dist/index.d.ts +1 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  21. package/dist/lib/drizzle/extensions/additionalActions.js +72 -22
  22. package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
  23. package/dist/utils/cacheTableUtils.d.ts +11 -0
  24. package/dist/utils/cacheTableUtils.d.ts.map +1 -0
  25. package/dist/utils/cacheTableUtils.js +450 -0
  26. package/dist/utils/cacheTableUtils.js.map +1 -0
  27. package/dist/utils/cacheUtils.d.ts.map +1 -1
  28. package/dist/utils/cacheUtils.js +3 -22
  29. package/dist/utils/cacheUtils.js.map +1 -1
  30. package/dist/utils/forgeDriver.d.ts +3 -2
  31. package/dist/utils/forgeDriver.d.ts.map +1 -1
  32. package/dist/utils/forgeDriver.js +24 -27
  33. package/dist/utils/forgeDriver.js.map +1 -1
  34. package/dist/utils/metadataContextUtils.d.ts +27 -1
  35. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  36. package/dist/utils/metadataContextUtils.js +237 -10
  37. package/dist/utils/metadataContextUtils.js.map +1 -1
  38. package/dist/utils/sqlUtils.d.ts +1 -0
  39. package/dist/utils/sqlUtils.d.ts.map +1 -1
  40. package/dist/utils/sqlUtils.js +217 -119
  41. package/dist/utils/sqlUtils.js.map +1 -1
  42. package/dist/webtriggers/applyMigrationsWebTrigger.js +1 -1
  43. package/dist/webtriggers/index.d.ts +1 -0
  44. package/dist/webtriggers/index.d.ts.map +1 -1
  45. package/dist/webtriggers/index.js +1 -0
  46. package/dist/webtriggers/index.js.map +1 -1
  47. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +60 -0
  48. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
  49. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js +55 -0
  50. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js.map +1 -0
  51. package/package.json +11 -10
  52. package/src/core/ForgeSQLORM.ts +78 -14
  53. package/src/core/ForgeSQLQueryBuilder.ts +15 -5
  54. package/src/core/ForgeSQLSelectOperations.ts +2 -1
  55. package/src/core/Rovo.ts +209 -167
  56. package/src/index.ts +1 -3
  57. package/src/lib/drizzle/extensions/additionalActions.ts +98 -42
  58. package/src/utils/cacheTableUtils.ts +511 -0
  59. package/src/utils/cacheUtils.ts +3 -25
  60. package/src/utils/forgeDriver.ts +38 -29
  61. package/src/utils/metadataContextUtils.ts +290 -10
  62. package/src/utils/sqlUtils.ts +298 -142
  63. package/src/webtriggers/applyMigrationsWebTrigger.ts +1 -1
  64. package/src/webtriggers/index.ts +1 -0
  65. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +69 -0
@@ -6,6 +6,7 @@ import { getTableName } from "drizzle-orm/table";
6
6
  import { Filter, FilterConditions, kvs, WhereConditions } from "@forge/kvs";
7
7
  import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
8
8
  import { cacheApplicationContext, isTableContainsTableInCacheContext } from "./cacheContextUtils";
9
+ import { extractBacktickedValues } from "./cacheTableUtils";
9
10
 
10
11
  // Constants for better maintainability
11
12
  const CACHE_CONSTANTS = {
@@ -47,29 +48,6 @@ function nowPlusSeconds(secondsToAdd: number): number {
47
48
  return Math.floor(dt.toSeconds());
48
49
  }
49
50
 
50
- /**
51
- * Extracts all table/column names between backticks from SQL query and returns them as comma-separated string.
52
- *
53
- * @param sql - SQL query string
54
- * @returns Comma-separated string of unique backticked values
55
- */
56
- function extractBacktickedValues(sql: string): string {
57
- const regex = /`([^`]+)`/g;
58
- const matches = new Set<string>();
59
- let match;
60
-
61
- while ((match = regex.exec(sql.toLowerCase())) !== null) {
62
- if (!match[1].startsWith("a_")) {
63
- matches.add(`\`${match[1]}\``);
64
- }
65
- }
66
-
67
- // Sort to ensure consistent order for the same input
68
- return Array.from(matches)
69
- .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }))
70
- .join(",");
71
- }
72
-
73
51
  /**
74
52
  * Generates a hash key for a query based on its SQL and parameters.
75
53
  *
@@ -362,7 +340,7 @@ export async function getFromCache<T>(
362
340
  if (
363
341
  cacheResult &&
364
342
  (cacheResult[expirationName] as number) >= getCurrentTime() &&
365
- extractBacktickedValues(sqlQuery.sql) === cacheResult[entityQueryName]
343
+ extractBacktickedValues(sqlQuery.sql, options) === cacheResult[entityQueryName]
366
344
  ) {
367
345
  if (options.logCache) {
368
346
  // eslint-disable-next-line no-console
@@ -423,7 +401,7 @@ export async function setCacheResult(
423
401
  .set(
424
402
  key,
425
403
  {
426
- [entityQueryName]: extractBacktickedValues(sqlQuery.sql),
404
+ [entityQueryName]: extractBacktickedValues(sqlQuery.sql, options),
427
405
  [expirationName]: nowPlusSeconds(cacheTtl),
428
406
  [dataName]: JSON.stringify(results),
429
407
  },
@@ -2,6 +2,7 @@ import { sql, UpdateQueryResponse } from "@forge/sql";
2
2
  import { saveMetaDataToContext } from "./metadataContextUtils";
3
3
  import { getOperationType } from "./requestTypeContextUtils";
4
4
  import { withTimeout } from "./sqlUtils";
5
+ import { SQL_API_ENDPOINTS } from "@forge/sql/out/sql";
5
6
 
6
7
  const timeoutMs = 10000;
7
8
  const timeoutMessage = `Atlassian @forge/sql did not return a response within ${timeoutMs}ms (${timeoutMs / 1000} seconds), so the request is blocked. Possible causes: slow query, network issues, or exceeding Forge SQL limits.`;
@@ -66,25 +67,23 @@ export function isUpdateQueryResponse(obj: unknown): obj is UpdateQueryResponse
66
67
  );
67
68
  }
68
69
 
69
- function inlineParams(sql: string, params: unknown[]): string {
70
- let i = 0;
71
- return sql.replace(/\?/g, () => {
72
- const val = params[i++];
73
- if (val === null) return "NULL";
74
- if (typeof val === "number") return val.toString();
75
- return `'${String(val).replace(/'/g, "''")}'`;
76
- });
77
- }
78
-
79
70
  /**
80
- * Processes DDL query results and saves metadata.
71
+ * Processes DDL query results and saves metadata to the execution context.
81
72
  *
73
+ * @param query - The SQL query string
74
+ * @param params - Query parameters
75
+ * @param method - Execution method ("all" or "execute")
82
76
  * @param result - The DDL query result
83
77
  * @returns Processed result for Drizzle ORM
84
78
  */
85
- async function processDDLResult(method: QueryMethod, result: any): Promise<ForgeDriverResult> {
79
+ async function processDDLResult(
80
+ query: string,
81
+ params: unknown[],
82
+ method: QueryMethod,
83
+ result: any,
84
+ ): Promise<ForgeDriverResult> {
86
85
  if (result.metadata) {
87
- await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
86
+ await saveMetaDataToContext(query, params, result.metadata as ForgeSQLMetadata);
88
87
  }
89
88
 
90
89
  if (!result?.rows) {
@@ -109,13 +108,16 @@ async function processDDLResult(method: QueryMethod, result: any): Promise<Forge
109
108
  }
110
109
 
111
110
  /**
112
- * Processes execute method results (UPDATE, INSERT, DELETE).
111
+ * Processes execute method results (UPDATE, INSERT, DELETE) and saves metadata to the execution context.
113
112
  *
114
- * @param query - The SQL query
115
- * @param params - Query parameters
113
+ * @param query - The SQL query string
114
+ * @param params - Query parameters (may be undefined)
116
115
  * @returns Processed result for Drizzle ORM
117
116
  */
118
- async function processExecuteMethod(query: string, params: unknown[]): Promise<ForgeDriverResult> {
117
+ async function processExecuteMethod(
118
+ query: string,
119
+ params: unknown[] | undefined,
120
+ ): Promise<ForgeDriverResult> {
119
121
  const sqlStatement = sql.prepare<UpdateQueryResponse>(query);
120
122
 
121
123
  if (params) {
@@ -123,7 +125,7 @@ async function processExecuteMethod(query: string, params: unknown[]): Promise<F
123
125
  }
124
126
 
125
127
  const result = await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs);
126
- await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
128
+ await saveMetaDataToContext(query, params ?? [], result.metadata as ForgeSQLMetadata);
127
129
  if (!result.rows) {
128
130
  return { rows: [[]] };
129
131
  }
@@ -132,13 +134,16 @@ async function processExecuteMethod(query: string, params: unknown[]): Promise<F
132
134
  }
133
135
 
134
136
  /**
135
- * Processes all method results (SELECT queries).
137
+ * Processes all method results (SELECT queries) and saves metadata to the execution context.
136
138
  *
137
- * @param query - The SQL query
138
- * @param params - Query parameters
139
+ * @param query - The SQL query string
140
+ * @param params - Query parameters (may be undefined)
139
141
  * @returns Processed result for Drizzle ORM
140
142
  */
141
- async function processAllMethod(query: string, params: unknown[]): Promise<ForgeDriverResult> {
143
+ async function processAllMethod(
144
+ query: string,
145
+ params: unknown[] | undefined,
146
+ ): Promise<ForgeDriverResult> {
142
147
  const sqlStatement = await sql.prepare<unknown>(query);
143
148
 
144
149
  if (params) {
@@ -146,7 +151,7 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
146
151
  }
147
152
 
148
153
  const result = await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs);
149
- await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
154
+ await saveMetaDataToContext(query, params ?? [], result.metadata as ForgeSQLMetadata);
150
155
 
151
156
  if (!result.rows) {
152
157
  return { rows: [] };
@@ -160,9 +165,10 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
160
165
  /**
161
166
  * Main Forge SQL driver function for Drizzle ORM integration.
162
167
  * Handles DDL operations, execute operations (UPDATE/INSERT/DELETE), and select operations.
168
+ * Automatically saves query execution metadata to the context for performance monitoring.
163
169
  *
164
170
  * @param query - The SQL query to execute
165
- * @param params - Query parameters
171
+ * @param params - Query parameters (may be undefined or empty array)
166
172
  * @param method - Execution method ("all" for SELECT, "execute" for UPDATE/INSERT/DELETE)
167
173
  * @returns Promise with query results compatible with Drizzle ORM
168
174
  *
@@ -182,25 +188,28 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
182
188
  */
183
189
  export const forgeDriver = async (
184
190
  query: string,
185
- params: unknown[],
191
+ params: unknown[] | undefined,
186
192
  method: QueryMethod,
187
193
  ): Promise<ForgeDriverResult> => {
188
194
  const operationType = await getOperationType();
189
195
  // Handle DDL operations
190
196
  if (operationType === "DDL") {
191
197
  const result = await withTimeout(
192
- sql.executeDDL(inlineParams(query, params)),
198
+ sql
199
+ .prepare(query, SQL_API_ENDPOINTS.EXECUTE_DDL)
200
+ .bindParams(params ?? [])
201
+ .execute(),
193
202
  timeoutMessage,
194
203
  timeoutMs,
195
204
  );
196
- return await processDDLResult(method, result);
205
+ return await processDDLResult(query, params ?? [], method, result);
197
206
  }
198
207
 
199
208
  // Handle execute method (UPDATE, INSERT, DELETE)
200
209
  if (method === "execute") {
201
- return await processExecuteMethod(query, params ?? []);
210
+ return await processExecuteMethod(query, params);
202
211
  }
203
212
 
204
213
  // Handle all method (SELECT)
205
- return await processAllMethod(query, params ?? []);
214
+ return await processAllMethod(query, params);
206
215
  };
@@ -1,32 +1,312 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { ForgeSQLMetadata } from "./forgeDriver";
3
3
  import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
4
+ import { ExplainAnalyzeRow } from "../core/SystemTables";
4
5
  import { printQueriesWithPlan } from "./sqlUtils";
6
+ import { Parser } from "node-sql-parser";
7
+
8
+ const DEFAULT_WINDOW_SIZE = 15 * 1000;
9
+
10
+ type Statistic = { query: string; params: unknown[]; metadata: ForgeSQLMetadata };
11
+
12
+ export type QueryPlanMode = "TopSlowest" | "SummaryTable";
13
+
14
+ export type MetadataQueryOptions = {
15
+ mode?: QueryPlanMode;
16
+ summaryTableWindowTime?: number;
17
+ topQueries?: number;
18
+ showSlowestPlans?: boolean;
19
+ normalizeQuery?: boolean;
20
+ };
5
21
 
6
22
  export type MetadataQueryContext = {
7
23
  totalDbExecutionTime: number;
8
24
  totalResponseSize: number;
9
25
  beginTime: Date;
26
+ options?: MetadataQueryOptions;
27
+ statistics: Statistic[];
10
28
  printQueriesWithPlan: () => Promise<void>;
11
29
  forgeSQLORM: ForgeSqlOperation;
12
30
  };
31
+
13
32
  export const metadataQueryContext = new AsyncLocalStorage<MetadataQueryContext>();
14
33
 
15
- export async function saveMetaDataToContext(metadata?: ForgeSQLMetadata): Promise<void> {
34
+ /**
35
+ * Creates default options for metadata query context.
36
+ * @returns Default options object
37
+ */
38
+ function createDefaultOptions(): Required<MetadataQueryOptions> {
39
+ return {
40
+ mode: "TopSlowest",
41
+ topQueries: 1,
42
+ summaryTableWindowTime: DEFAULT_WINDOW_SIZE,
43
+ showSlowestPlans: true,
44
+ normalizeQuery: true,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Merges provided options with defaults, using defaults only for undefined fields.
50
+ * @param options - Optional partial options to merge
51
+ * @returns Complete options object with all fields set
52
+ */
53
+ function mergeOptionsWithDefaults(options?: MetadataQueryOptions): Required<MetadataQueryOptions> {
54
+ const defaults = createDefaultOptions();
55
+ return {
56
+ mode: options?.mode ?? defaults.mode,
57
+ topQueries: options?.topQueries ?? defaults.topQueries,
58
+ summaryTableWindowTime: options?.summaryTableWindowTime ?? defaults.summaryTableWindowTime,
59
+ showSlowestPlans: options?.showSlowestPlans ?? defaults.showSlowestPlans,
60
+ normalizeQuery: options?.normalizeQuery ?? defaults.normalizeQuery,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Normalizes SQL query using regex fallback by replacing parameter values with placeholders.
66
+ * Replaces string literals, numeric values, and boolean values with '?' for logging.
67
+ *
68
+ * Note: This is a fallback function used when node-sql-parser fails.
69
+ * It uses simple, safe regex patterns to avoid ReDoS (Regular Expression Denial of Service) vulnerabilities.
70
+ * For proper handling of escaped quotes and complex SQL, use the main normalizeSqlForLogging function
71
+ * which uses node-sql-parser.
72
+ *
73
+ * @param sql - SQL query string to normalize
74
+ * @returns Normalized SQL string with parameters replaced by '?'
75
+ */
76
+ function normalizeSqlForLoggingRegex(sql: string): string {
77
+ let normalized = sql;
78
+
79
+ // Replace string literals (single quotes) - using simple greedy match
80
+ // This avoids catastrophic backtracking by using a simple [^']* pattern
81
+ // Note: This does not handle SQL-style escaped quotes (doubled quotes: '')
82
+ // For proper handling, use the main normalizeSqlForLogging function with node-sql-parser
83
+ normalized = normalized.replace(/'[^']*'/g, "?");
84
+
85
+ // Replace string literals (double quotes) - using simple greedy match
86
+ // Same safety considerations as above
87
+ normalized = normalized.replace(/"[^"]*"/g, "?");
88
+
89
+ // Replace numeric literals - simplified pattern to avoid backtracking
90
+ // Match: optional minus, digits, optional decimal point and more digits
91
+ // Using word boundaries (\b) for safety - avoids complex lookahead/lookbehind
92
+ normalized = normalized.replace(/\b-?\d+\.?\d*\b/g, "?");
93
+
94
+ // Replace boolean literals - safe pattern with word boundaries
95
+ // Simple alternation with word boundaries - no nested quantifiers
96
+ normalized = normalized.replace(/\b(true|false)\b/gi, "?");
97
+
98
+ // Replace NULL values (but be careful not to replace in identifiers)
99
+ // Simple word boundary match - safe from backtracking
100
+ normalized = normalized.replace(/\bNULL\b/gi, "?");
101
+
102
+ return normalized;
103
+ }
104
+
105
+ /**
106
+ * Normalizes SQL query by replacing parameter values with placeholders.
107
+ * First attempts to use node-sql-parser for structure normalization, then applies regex for value replacement.
108
+ * Falls back to regex-based normalization if parsing fails.
109
+ * @param sql - SQL query string to normalize
110
+ * @returns Normalized SQL string with parameters replaced by '?'
111
+ */
112
+ function normalizeSqlForLogging(sql: string): string {
113
+ try {
114
+ const parser = new Parser();
115
+ const ast = parser.astify(sql.trim());
116
+
117
+ // Convert AST back to SQL (this normalizes structure and formatting)
118
+ const normalized = parser.sqlify(Array.isArray(ast) ? ast[0] : ast);
119
+
120
+ // Apply regex-based value replacement to the normalized SQL
121
+ // This handles the case where sqlify might preserve some literal values
122
+ let result = normalizeSqlForLoggingRegex(normalized.trim());
123
+
124
+ // Remove backticks added by sqlify for cleaner logging (optional - can be removed if backticks are preferred)
125
+ result = result.replace(/`/g, "");
126
+
127
+ return result;
128
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
129
+ } catch (e) {
130
+ // If parsing fails, fall back to regex-based normalization
131
+ return normalizeSqlForLoggingRegex(sql);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Formats row information (estRows, actRows) into a string.
137
+ * @param row - ExplainAnalyzeRow object
138
+ * @returns Formatted row info string or null if no row info available
139
+ */
140
+ function formatRowInfo(row: ExplainAnalyzeRow): string | null {
141
+ const rowInfo: string[] = [];
142
+ if (row.estRows) rowInfo.push(`estRows:${row.estRows}`);
143
+ if (row.actRows) rowInfo.push(`actRows:${row.actRows}`);
144
+ return rowInfo.length > 0 ? `[${rowInfo.join(", ")}]` : null;
145
+ }
146
+
147
+ /**
148
+ * Formats resource information (memory, disk) into a string.
149
+ * @param row - ExplainAnalyzeRow object
150
+ * @returns Formatted resource info string or null if no resource info available
151
+ */
152
+ function formatResourceInfo(row: ExplainAnalyzeRow): string | null {
153
+ const resourceInfo: string[] = [];
154
+ if (row.memory) resourceInfo.push(`memory:${row.memory}`);
155
+ if (row.disk) resourceInfo.push(`disk:${row.disk}`);
156
+ return resourceInfo.length > 0 ? `(${resourceInfo.join(", ")})` : null;
157
+ }
158
+
159
+ /**
160
+ * Formats a single execution plan row into a string.
161
+ * @param row - ExplainAnalyzeRow object
162
+ * @returns Formatted string representation of the row
163
+ */
164
+ function formatPlanRow(row: ExplainAnalyzeRow): string {
165
+ const parts: string[] = [];
166
+
167
+ if (row.id) parts.push(row.id);
168
+ if (row.task) parts.push(`task:${row.task}`);
169
+ if (row.operatorInfo) parts.push(row.operatorInfo);
170
+
171
+ const rowInfo = formatRowInfo(row);
172
+ if (rowInfo) parts.push(rowInfo);
173
+
174
+ if (row.executionInfo) parts.push(`execution info:${row.executionInfo}`);
175
+
176
+ const resourceInfo = formatResourceInfo(row);
177
+ if (resourceInfo) parts.push(resourceInfo);
178
+
179
+ if (row.accessObject) parts.push(`access object:${row.accessObject}`);
180
+
181
+ return parts.join(" | ");
182
+ }
183
+
184
+ /**
185
+ * Formats an execution plan array into a readable string representation.
186
+ * @param planRows - Array of ExplainAnalyzeRow objects representing the execution plan
187
+ * @returns Formatted string representation of the execution plan
188
+ */
189
+ function formatExplainPlan(planRows: ExplainAnalyzeRow[]): string {
190
+ if (!planRows || planRows.length === 0) {
191
+ return "No execution plan available";
192
+ }
193
+
194
+ return planRows.map(formatPlanRow).join("\n");
195
+ }
196
+
197
+ /**
198
+ * Prints query plans using summary tables if mode is SummaryTable and within time window.
199
+ * @param context - The metadata query context
200
+ * @param options - The merged options with defaults
201
+ * @returns Promise that resolves when plans are printed
202
+ */
203
+ async function printPlansUsingSummaryTables(
204
+ context: MetadataQueryContext,
205
+ options: Required<MetadataQueryOptions>,
206
+ ): Promise<boolean> {
207
+ const timeDiff = Date.now() - context.beginTime.getTime();
208
+
209
+ if (options.mode !== "SummaryTable") {
210
+ return false;
211
+ }
212
+
213
+ if (timeDiff <= options.summaryTableWindowTime) {
214
+ await new Promise((resolve) => setTimeout(resolve, 200));
215
+ const summaryTableDiffMs = Date.now() - context.beginTime.getTime();
216
+ await printQueriesWithPlan(context.forgeSQLORM, summaryTableDiffMs);
217
+ return true;
218
+ }
219
+ // eslint-disable-next-line no-console
220
+ console.warn("Summary table window expired — showing query plans instead");
221
+ return false;
222
+ }
223
+
224
+ /**
225
+ * Prints query plans for the top slowest queries.
226
+ * @param context - The metadata query context
227
+ * @param options - The merged options with defaults
228
+ * @returns Promise that resolves when plans are printed
229
+ */
230
+ async function printTopQueriesPlans(
231
+ context: MetadataQueryContext,
232
+ options: Required<MetadataQueryOptions>,
233
+ ): Promise<void> {
234
+ const topQueries = context.statistics
235
+ .toSorted((a, b) => b.metadata.dbExecutionTime - a.metadata.dbExecutionTime)
236
+ .slice(0, options.topQueries);
237
+
238
+ for (const query of topQueries) {
239
+ const normalizedQuery = options.normalizeQuery
240
+ ? normalizeSqlForLogging(query.query)
241
+ : query.query;
242
+ if (options.showSlowestPlans) {
243
+ const explainAnalyzeRows = await context.forgeSQLORM
244
+ .analyze()
245
+ .explainAnalyzeRaw(query.query, query.params);
246
+ const formattedPlan = formatExplainPlan(explainAnalyzeRows);
247
+ // eslint-disable-next-line no-console
248
+ console.warn(
249
+ `SQL: ${normalizedQuery} | Time: ${query.metadata.dbExecutionTime} ms\n Plan:\n${formattedPlan}`,
250
+ );
251
+ } else {
252
+ // eslint-disable-next-line no-console
253
+ console.warn(`SQL: ${normalizedQuery} | Time: ${query.metadata.dbExecutionTime} ms`);
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Saves query metadata to the current context and sets up the printQueriesWithPlan function.
260
+ * @param stringQuery - The SQL query string
261
+ * @param params - Query parameters
262
+ * @param metadata - Query execution metadata
263
+ */
264
+ export async function saveMetaDataToContext(
265
+ stringQuery: string,
266
+ params: unknown[],
267
+ metadata: ForgeSQLMetadata,
268
+ ): Promise<void> {
16
269
  const context = metadataQueryContext.getStore();
17
- if (context) {
18
- context.printQueriesWithPlan = async () => {
19
- await new Promise((r) => setTimeout(r, 200));
20
- await printQueriesWithPlan(context.forgeSQLORM, Date.now() - context.beginTime.getTime());
21
- };
22
- if (metadata) {
23
- context.totalResponseSize += metadata.responseSize;
24
- context.totalDbExecutionTime += metadata.dbExecutionTime;
270
+ if (!context) {
271
+ return;
272
+ }
273
+
274
+ // Initialize statistics array if needed
275
+ if (!context.statistics) {
276
+ context.statistics = [];
277
+ }
278
+
279
+ // Merge options with defaults
280
+ context.options = mergeOptionsWithDefaults(context.options);
281
+
282
+ // Add query statistics
283
+ context.statistics.push({ query: stringQuery, params, metadata });
284
+
285
+ // Set up printQueriesWithPlan function
286
+ context.printQueriesWithPlan = async () => {
287
+ const options = mergeOptionsWithDefaults(context.options);
288
+
289
+ // Try to use summary tables first if enabled
290
+ const usedSummaryTables = await printPlansUsingSummaryTables(context, options);
291
+ if (usedSummaryTables) {
292
+ return;
25
293
  }
26
- // Log the results to console
294
+
295
+ // Fall back to printing top queries plans
296
+ await printTopQueriesPlans(context, options);
297
+ };
298
+
299
+ // Update aggregated metrics
300
+ if (metadata) {
301
+ context.totalResponseSize += metadata.responseSize;
302
+ context.totalDbExecutionTime += metadata.dbExecutionTime;
27
303
  }
28
304
  }
29
305
 
306
+ /**
307
+ * Gets the latest metadata from the current context.
308
+ * @returns The current metadata context or undefined if not in a context
309
+ */
30
310
  export async function getLastestMetadata(): Promise<MetadataQueryContext | undefined> {
31
311
  return metadataQueryContext.getStore();
32
312
  }