forge-sql-orm 2.1.15 → 2.1.17

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 (57) hide show
  1. package/README.md +194 -1
  2. package/dist/async/PrintQueryConsumer.d.ts +98 -0
  3. package/dist/async/PrintQueryConsumer.d.ts.map +1 -0
  4. package/dist/async/PrintQueryConsumer.js +89 -0
  5. package/dist/async/PrintQueryConsumer.js.map +1 -0
  6. package/dist/core/ForgeSQLQueryBuilder.d.ts +2 -3
  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 +2 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -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.map +1 -1
  31. package/dist/utils/forgeDriver.js +5 -12
  32. package/dist/utils/forgeDriver.js.map +1 -1
  33. package/dist/utils/forgeDriverProxy.js +7 -5
  34. package/dist/utils/forgeDriverProxy.js.map +1 -1
  35. package/dist/utils/metadataContextUtils.d.ts +44 -4
  36. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  37. package/dist/utils/metadataContextUtils.js +155 -50
  38. package/dist/utils/metadataContextUtils.js.map +1 -1
  39. package/dist/utils/sqlUtils.d.ts +3 -1
  40. package/dist/utils/sqlUtils.d.ts.map +1 -1
  41. package/dist/utils/sqlUtils.js +264 -144
  42. package/dist/utils/sqlUtils.js.map +1 -1
  43. package/dist/webtriggers/applyMigrationsWebTrigger.js +1 -1
  44. package/package.json +14 -13
  45. package/src/async/PrintQueryConsumer.ts +114 -0
  46. package/src/core/ForgeSQLQueryBuilder.ts +2 -2
  47. package/src/core/ForgeSQLSelectOperations.ts +2 -1
  48. package/src/core/Rovo.ts +209 -167
  49. package/src/index.ts +2 -3
  50. package/src/lib/drizzle/extensions/additionalActions.ts +98 -42
  51. package/src/utils/cacheTableUtils.ts +511 -0
  52. package/src/utils/cacheUtils.ts +3 -25
  53. package/src/utils/forgeDriver.ts +5 -11
  54. package/src/utils/forgeDriverProxy.ts +9 -9
  55. package/src/utils/metadataContextUtils.ts +169 -52
  56. package/src/utils/sqlUtils.ts +372 -177
  57. package/src/webtriggers/applyMigrationsWebTrigger.ts +1 -1
@@ -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,16 +67,6 @@ 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
71
  * Processes DDL query results and saves metadata to the execution context.
81
72
  *
@@ -204,7 +195,10 @@ export const forgeDriver = async (
204
195
  // Handle DDL operations
205
196
  if (operationType === "DDL") {
206
197
  const result = await withTimeout(
207
- sql.executeDDL(inlineParams(query, params ?? [])),
198
+ sql
199
+ .prepare(query, SQL_API_ENDPOINTS.EXECUTE_DDL)
200
+ .bindParams(params ?? [])
201
+ .execute(),
208
202
  timeoutMessage,
209
203
  timeoutMs,
210
204
  );
@@ -1,7 +1,7 @@
1
1
  import { forgeDriver } from "./forgeDriver";
2
2
  import { injectSqlHints, SqlHints } from "./sqlHints";
3
3
  import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
4
- import { printQueriesWithPlan } from "./sqlUtils";
4
+ import { handleErrorsWithPlan } from "./sqlUtils";
5
5
 
6
6
  /**
7
7
  * Error codes and constants for query analysis
@@ -57,22 +57,22 @@ export function createForgeDriverProxy(
57
57
  error?.context?.debug?.errno === QUERY_ERROR_CODES.OUT_OF_MEMORY_ERRNO;
58
58
 
59
59
  if (isTimeoutError || isOutOfMemoryError) {
60
+ // Wait for CLUSTER_STATEMENTS_SUMMARY to be populated with our failed query data
61
+ await new Promise((resolve) => setTimeout(resolve, STATEMENTS_SUMMARY_DELAY_MS));
62
+
63
+ const queryEndTime = Date.now();
64
+ const queryDuration = queryEndTime - queryStartTime;
65
+ let errorType: "OOM" | "TIMEOUT" = "TIMEOUT";
60
66
  if (isTimeoutError) {
61
67
  // eslint-disable-next-line no-console
62
68
  console.error(` TIMEOUT detected - Query exceeded time limit`);
63
69
  } else {
64
70
  // eslint-disable-next-line no-console
65
71
  console.error(`OUT OF MEMORY detected - Query exceeded memory limit`);
72
+ errorType = "OOM";
66
73
  }
67
-
68
- // Wait for CLUSTER_STATEMENTS_SUMMARY to be populated with our failed query data
69
- await new Promise((resolve) => setTimeout(resolve, STATEMENTS_SUMMARY_DELAY_MS));
70
-
71
- const queryEndTime = Date.now();
72
- const queryDuration = queryEndTime - queryStartTime;
73
-
74
74
  // Analyze the failed query using CLUSTER_STATEMENTS_SUMMARY
75
- await printQueriesWithPlan(forgeSqlOperation, queryDuration);
75
+ await handleErrorsWithPlan(forgeSqlOperation, queryDuration, errorType);
76
76
  }
77
77
 
78
78
  // Log SQL error details if requested
@@ -2,12 +2,15 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { ForgeSQLMetadata } from "./forgeDriver";
3
3
  import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
4
4
  import { ExplainAnalyzeRow } from "../core/SystemTables";
5
- import { printQueriesWithPlan } from "./sqlUtils";
5
+ import { printQueriesWithPlan, withTimeout } from "./sqlUtils";
6
6
  import { Parser } from "node-sql-parser";
7
+ import { PushResult, Queue } from "@forge/events";
8
+ import { AsyncEventPrintQuery } from "../async/PrintQueryConsumer";
7
9
 
10
+ const TIMEOUT_ASYNC_EVENT_SENT = 1200;
8
11
  const DEFAULT_WINDOW_SIZE = 15 * 1000;
9
12
 
10
- type Statistic = { query: string; params: unknown[]; metadata: ForgeSQLMetadata };
13
+ export type Statistic = { query: string; params: unknown[]; metadata: ForgeSQLMetadata };
11
14
 
12
15
  export type QueryPlanMode = "TopSlowest" | "SummaryTable";
13
16
 
@@ -17,6 +20,7 @@ export type MetadataQueryOptions = {
17
20
  topQueries?: number;
18
21
  showSlowestPlans?: boolean;
19
22
  normalizeQuery?: boolean;
23
+ asyncQueueName?: string;
20
24
  };
21
25
 
22
26
  export type MetadataQueryContext = {
@@ -42,6 +46,7 @@ function createDefaultOptions(): Required<MetadataQueryOptions> {
42
46
  summaryTableWindowTime: DEFAULT_WINDOW_SIZE,
43
47
  showSlowestPlans: true,
44
48
  normalizeQuery: true,
49
+ asyncQueueName: "",
45
50
  };
46
51
  }
47
52
 
@@ -58,6 +63,7 @@ function mergeOptionsWithDefaults(options?: MetadataQueryOptions): Required<Meta
58
63
  summaryTableWindowTime: options?.summaryTableWindowTime ?? defaults.summaryTableWindowTime,
59
64
  showSlowestPlans: options?.showSlowestPlans ?? defaults.showSlowestPlans,
60
65
  normalizeQuery: options?.normalizeQuery ?? defaults.normalizeQuery,
66
+ asyncQueueName: options?.asyncQueueName ?? defaults.asyncQueueName,
61
67
  };
62
68
  }
63
69
 
@@ -133,56 +139,84 @@ function normalizeSqlForLogging(sql: string): string {
133
139
  }
134
140
 
135
141
  /**
136
- * Formats an execution plan array into a readable string representation.
137
- * @param planRows - Array of ExplainAnalyzeRow objects representing the execution plan
138
- * @returns Formatted string representation of the execution plan
142
+ * Formats row information (estRows, actRows) into a string.
143
+ * @param row - ExplainAnalyzeRow object
144
+ * @returns Formatted row info string or null if no row info available
139
145
  */
140
- function formatExplainPlan(planRows: ExplainAnalyzeRow[]): string {
141
- if (!planRows || planRows.length === 0) {
142
- return "No execution plan available";
143
- }
146
+ function formatRowInfo(row: ExplainAnalyzeRow): string | null {
147
+ const rowInfo: string[] = [];
148
+ if (row.estRows) rowInfo.push(`estRows:${row.estRows}`);
149
+ if (row.actRows) rowInfo.push(`actRows:${row.actRows}`);
150
+ return rowInfo.length > 0 ? `[${rowInfo.join(", ")}]` : null;
151
+ }
144
152
 
145
- const lines: string[] = [];
153
+ /**
154
+ * Formats resource information (memory, disk) into a string.
155
+ * @param row - ExplainAnalyzeRow object
156
+ * @returns Formatted resource info string or null if no resource info available
157
+ */
158
+ function formatResourceInfo(row: ExplainAnalyzeRow): string | null {
159
+ const resourceInfo: string[] = [];
160
+ if (row.memory) resourceInfo.push(`memory:${row.memory}`);
161
+ if (row.disk) resourceInfo.push(`disk:${row.disk}`);
162
+ return resourceInfo.length > 0 ? `(${resourceInfo.join(", ")})` : null;
163
+ }
146
164
 
147
- for (const row of planRows) {
148
- const parts: string[] = [];
165
+ /**
166
+ * Formats a single execution plan row into a string.
167
+ * @param row - ExplainAnalyzeRow object
168
+ * @returns Formatted string representation of the row
169
+ */
170
+ function formatPlanRow(row: ExplainAnalyzeRow): string {
171
+ const parts: string[] = [];
172
+
173
+ if (row.id) parts.push(row.id);
174
+ if (row.task) parts.push(`task:${row.task}`);
175
+ if (row.operatorInfo) parts.push(row.operatorInfo);
149
176
 
150
- if (row.id) parts.push(row.id);
151
- if (row.task) parts.push(`task:${row.task}`);
152
- if (row.operatorInfo) parts.push(row.operatorInfo);
177
+ const rowInfo = formatRowInfo(row);
178
+ if (rowInfo) parts.push(rowInfo);
153
179
 
154
- const rowInfo: string[] = [];
155
- if (row.estRows) rowInfo.push(`estRows:${row.estRows}`);
156
- if (row.actRows) rowInfo.push(`actRows:${row.actRows}`);
157
- if (rowInfo.length > 0) parts.push(`[${rowInfo.join(", ")}]`);
180
+ if (row.executionInfo) parts.push(`execution info:${row.executionInfo}`);
158
181
 
159
- if (row.executionInfo) parts.push(`execution info:${row.executionInfo}`);
182
+ const resourceInfo = formatResourceInfo(row);
183
+ if (resourceInfo) parts.push(resourceInfo);
160
184
 
161
- const resourceInfo: string[] = [];
162
- if (row.memory) resourceInfo.push(`memory:${row.memory}`);
163
- if (row.disk) resourceInfo.push(`disk:${row.disk}`);
164
- if (resourceInfo.length > 0) parts.push(`(${resourceInfo.join(", ")})`);
185
+ if (row.accessObject) parts.push(`access object:${row.accessObject}`);
165
186
 
166
- if (row.accessObject) parts.push(`access object:${row.accessObject}`);
187
+ return parts.join(" | ");
188
+ }
167
189
 
168
- lines.push(parts.join(" | "));
190
+ /**
191
+ * Formats an execution plan array into a readable string representation.
192
+ * @param planRows - Array of ExplainAnalyzeRow objects representing the execution plan
193
+ * @returns Formatted string representation of the execution plan
194
+ */
195
+ function formatExplainPlan(planRows: ExplainAnalyzeRow[]): string {
196
+ if (!planRows || planRows.length === 0) {
197
+ return "No execution plan available";
169
198
  }
170
199
 
171
- return lines.join("\n");
200
+ return planRows.map(formatPlanRow).join("\n");
172
201
  }
173
202
 
174
203
  /**
175
204
  * Prints query plans using summary tables if mode is SummaryTable and within time window.
176
- * @param context - The metadata query context
177
- * @param options - The merged options with defaults
178
- * @returns Promise that resolves when plans are printed
205
+ *
206
+ * Attempts to use CLUSTER_STATEMENTS_SUMMARY table for query analysis if:
207
+ * - Mode is set to "SummaryTable"
208
+ * - Time since query execution start is within the configured window
209
+ *
210
+ * @param context - The async event payload containing query statistics and options
211
+ * @param forgeSQLORM - The ForgeSQL operation instance for database access
212
+ * @returns Promise that resolves to true if summary tables were used, false otherwise
179
213
  */
180
214
  async function printPlansUsingSummaryTables(
181
- context: MetadataQueryContext,
182
- options: Required<MetadataQueryOptions>,
215
+ context: AsyncEventPrintQuery,
216
+ forgeSQLORM: ForgeSqlOperation,
183
217
  ): Promise<boolean> {
184
218
  const timeDiff = Date.now() - context.beginTime.getTime();
185
-
219
+ const options = context.options;
186
220
  if (options.mode !== "SummaryTable") {
187
221
  return false;
188
222
  }
@@ -190,7 +224,7 @@ async function printPlansUsingSummaryTables(
190
224
  if (timeDiff <= options.summaryTableWindowTime) {
191
225
  await new Promise((resolve) => setTimeout(resolve, 200));
192
226
  const summaryTableDiffMs = Date.now() - context.beginTime.getTime();
193
- await printQueriesWithPlan(context.forgeSQLORM, summaryTableDiffMs);
227
+ await printQueriesWithPlan(forgeSQLORM, summaryTableDiffMs);
194
228
  return true;
195
229
  }
196
230
  // eslint-disable-next-line no-console
@@ -199,17 +233,22 @@ async function printPlansUsingSummaryTables(
199
233
  }
200
234
 
201
235
  /**
202
- * Prints query plans for the top slowest queries.
203
- * @param context - The metadata query context
204
- * @param options - The merged options with defaults
205
- * @returns Promise that resolves when plans are printed
236
+ * Prints query plans for the top slowest queries from the statistics.
237
+ *
238
+ * Sorts queries by execution time and prints the top N queries (based on topQueries option).
239
+ * For each query, it can optionally print the execution plan using EXPLAIN ANALYZE.
240
+ *
241
+ * @param context - The async event payload containing query statistics and options
242
+ * @param forgeSQLORM - The ForgeSQL operation instance for database access
243
+ * @returns Promise that resolves when all query plans are printed
206
244
  */
207
245
  async function printTopQueriesPlans(
208
- context: MetadataQueryContext,
209
- options: Required<MetadataQueryOptions>,
246
+ context: AsyncEventPrintQuery,
247
+ forgeSQLORM: ForgeSqlOperation,
210
248
  ): Promise<void> {
249
+ const options = context.options;
211
250
  const topQueries = context.statistics
212
- .sort((a, b) => b.metadata.dbExecutionTime - a.metadata.dbExecutionTime)
251
+ .toSorted((a, b) => b.metadata.dbExecutionTime - a.metadata.dbExecutionTime)
213
252
  .slice(0, options.topQueries);
214
253
 
215
254
  for (const query of topQueries) {
@@ -217,7 +256,7 @@ async function printTopQueriesPlans(
217
256
  ? normalizeSqlForLogging(query.query)
218
257
  : query.query;
219
258
  if (options.showSlowestPlans) {
220
- const explainAnalyzeRows = await context.forgeSQLORM
259
+ const explainAnalyzeRows = await forgeSQLORM
221
260
  .analyze()
222
261
  .explainAnalyzeRaw(query.query, query.params);
223
262
  const formattedPlan = formatExplainPlan(explainAnalyzeRows);
@@ -234,9 +273,33 @@ async function printTopQueriesPlans(
234
273
 
235
274
  /**
236
275
  * Saves query metadata to the current context and sets up the printQueriesWithPlan function.
276
+ *
277
+ * This function accumulates query statistics in the async context. When printQueriesWithPlan
278
+ * is called, it can either:
279
+ * - Queue the analysis for async processing (if asyncQueueName is provided)
280
+ * - Execute the analysis synchronously (fallback or if asyncQueueName is not set)
281
+ *
282
+ * For async processing, the function sends an event to the specified queue with a timeout.
283
+ * If the event cannot be sent within the timeout, it falls back to synchronous execution.
284
+ *
237
285
  * @param stringQuery - The SQL query string
238
- * @param params - Query parameters
239
- * @param metadata - Query execution metadata
286
+ * @param params - Query parameters used in the query
287
+ * @param metadata - Query execution metadata including execution time and response size
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * await FORGE_SQL_ORM.executeWithMetadata(
292
+ * async () => {
293
+ * // ... queries ...
294
+ * },
295
+ * async (totalDbExecutionTime, totalResponseSize, printQueries) => {
296
+ * if (totalDbExecutionTime > threshold) {
297
+ * await printQueries(); // Will use async queue if configured
298
+ * }
299
+ * },
300
+ * { asyncQueueName: "degradationQueue" }
301
+ * );
302
+ * ```
240
303
  */
241
304
  export async function saveMetaDataToContext(
242
305
  stringQuery: string,
@@ -262,15 +325,41 @@ export async function saveMetaDataToContext(
262
325
  // Set up printQueriesWithPlan function
263
326
  context.printQueriesWithPlan = async () => {
264
327
  const options = mergeOptionsWithDefaults(context.options);
265
-
266
- // Try to use summary tables first if enabled
267
- const usedSummaryTables = await printPlansUsingSummaryTables(context, options);
268
- if (usedSummaryTables) {
269
- return;
328
+ const param: AsyncEventPrintQuery = {
329
+ statistics: context.statistics,
330
+ totalDbExecutionTime: context.totalDbExecutionTime,
331
+ totalResponseSize: context.totalResponseSize,
332
+ beginTime: context.beginTime,
333
+ options,
334
+ };
335
+ if (options.asyncQueueName) {
336
+ const queue = new Queue({ key: options.asyncQueueName });
337
+ try {
338
+ const eventInfo = await withTimeout<PushResult>(
339
+ queue.push({
340
+ body: param,
341
+ concurrency: {
342
+ key: "orm_" + options.asyncQueueName,
343
+ limit: 2,
344
+ },
345
+ }),
346
+ `Event was not sent within ${TIMEOUT_ASYNC_EVENT_SENT}ms`,
347
+ TIMEOUT_ASYNC_EVENT_SENT,
348
+ );
349
+ // eslint-disable-next-line no-console
350
+ console.warn(
351
+ `[Performance Analysis] Query degradation event queued for async processing | Job ID: ${eventInfo.jobId} | Total DB time: ${context.totalDbExecutionTime}ms | Queries: ${context.statistics.length} | Look for consumer log with jobId: ${eventInfo.jobId}`,
352
+ );
353
+ return;
354
+ } catch (e: any) {
355
+ // eslint-disable-next-line no-console
356
+ console.warn(
357
+ "Async printing failed — falling back to synchronous execution: " + e.message,
358
+ e,
359
+ );
360
+ }
270
361
  }
271
-
272
- // Fall back to printing top queries plans
273
- await printTopQueriesPlans(context, options);
362
+ await printDegradationQueries(context.forgeSQLORM, param);
274
363
  };
275
364
 
276
365
  // Update aggregated metrics
@@ -280,6 +369,34 @@ export async function saveMetaDataToContext(
280
369
  }
281
370
  }
282
371
 
372
+ /**
373
+ * Prints query degradation analysis for the provided event payload.
374
+ *
375
+ * This function processes query degradation events (either from async queue or synchronous call).
376
+ * It first attempts to use summary tables (CLUSTER_STATEMENTS_SUMMARY) if configured and within
377
+ * the time window. Otherwise, it falls back to printing execution plans for the top slowest queries.
378
+ *
379
+ * @param forgeSQLORM - The ForgeSQL operation instance for database access
380
+ * @param params - The async event payload containing query statistics, options, and metadata
381
+ * @returns Promise that resolves when query analysis is complete
382
+ *
383
+ * @see printPlansUsingSummaryTables - For summary table analysis
384
+ * @see printTopQueriesPlans - For top slowest queries analysis
385
+ */
386
+ export async function printDegradationQueries(
387
+ forgeSQLORM: ForgeSqlOperation,
388
+ params: AsyncEventPrintQuery,
389
+ ): Promise<void> {
390
+ // Try to use summary tables first if enabled
391
+ const usedSummaryTables = await printPlansUsingSummaryTables(params, forgeSQLORM);
392
+ if (usedSummaryTables) {
393
+ return;
394
+ }
395
+
396
+ // Fall back to printing top queries plans
397
+ await printTopQueriesPlans(params, forgeSQLORM);
398
+ }
399
+
283
400
  /**
284
401
  * Gets the latest metadata from the current context.
285
402
  * @returns The current metadata context or undefined if not in a context