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.
- package/README.md +194 -1
- package/dist/async/PrintQueryConsumer.d.ts +98 -0
- package/dist/async/PrintQueryConsumer.d.ts.map +1 -0
- package/dist/async/PrintQueryConsumer.js +89 -0
- package/dist/async/PrintQueryConsumer.js.map +1 -0
- package/dist/core/ForgeSQLQueryBuilder.d.ts +2 -3
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts +2 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.js.map +1 -1
- package/dist/core/Rovo.d.ts +40 -0
- package/dist/core/Rovo.d.ts.map +1 -1
- package/dist/core/Rovo.js +164 -138
- package/dist/core/Rovo.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +72 -22
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
- package/dist/utils/cacheTableUtils.d.ts +11 -0
- package/dist/utils/cacheTableUtils.d.ts.map +1 -0
- package/dist/utils/cacheTableUtils.js +450 -0
- package/dist/utils/cacheTableUtils.js.map +1 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +3 -22
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +5 -12
- package/dist/utils/forgeDriver.js.map +1 -1
- package/dist/utils/forgeDriverProxy.js +7 -5
- package/dist/utils/forgeDriverProxy.js.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +44 -4
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.js +155 -50
- package/dist/utils/metadataContextUtils.js.map +1 -1
- package/dist/utils/sqlUtils.d.ts +3 -1
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +264 -144
- package/dist/utils/sqlUtils.js.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.js +1 -1
- package/package.json +14 -13
- package/src/async/PrintQueryConsumer.ts +114 -0
- package/src/core/ForgeSQLQueryBuilder.ts +2 -2
- package/src/core/ForgeSQLSelectOperations.ts +2 -1
- package/src/core/Rovo.ts +209 -167
- package/src/index.ts +2 -3
- package/src/lib/drizzle/extensions/additionalActions.ts +98 -42
- package/src/utils/cacheTableUtils.ts +511 -0
- package/src/utils/cacheUtils.ts +3 -25
- package/src/utils/forgeDriver.ts +5 -11
- package/src/utils/forgeDriverProxy.ts +9 -9
- package/src/utils/metadataContextUtils.ts +169 -52
- package/src/utils/sqlUtils.ts +372 -177
- package/src/webtriggers/applyMigrationsWebTrigger.ts +1 -1
package/src/utils/cacheUtils.ts
CHANGED
|
@@ -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
|
},
|
package/src/utils/forgeDriver.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
|
137
|
-
* @param
|
|
138
|
-
* @returns Formatted string
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
if (row.operatorInfo) parts.push(row.operatorInfo);
|
|
177
|
+
const rowInfo = formatRowInfo(row);
|
|
178
|
+
if (rowInfo) parts.push(rowInfo);
|
|
153
179
|
|
|
154
|
-
|
|
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
|
-
|
|
182
|
+
const resourceInfo = formatResourceInfo(row);
|
|
183
|
+
if (resourceInfo) parts.push(resourceInfo);
|
|
160
184
|
|
|
161
|
-
|
|
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
|
-
|
|
187
|
+
return parts.join(" | ");
|
|
188
|
+
}
|
|
167
189
|
|
|
168
|
-
|
|
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
|
|
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
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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:
|
|
182
|
-
|
|
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(
|
|
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
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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:
|
|
209
|
-
|
|
246
|
+
context: AsyncEventPrintQuery,
|
|
247
|
+
forgeSQLORM: ForgeSqlOperation,
|
|
210
248
|
): Promise<void> {
|
|
249
|
+
const options = context.options;
|
|
211
250
|
const topQueries = context.statistics
|
|
212
|
-
.
|
|
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
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|