forge-sql-orm 2.1.13 → 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 (36) hide show
  1. package/README.md +550 -21
  2. package/dist/core/ForgeSQLORM.d.ts +45 -8
  3. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  4. package/dist/core/ForgeSQLORM.js +134 -15
  5. package/dist/core/ForgeSQLORM.js.map +1 -1
  6. package/dist/core/ForgeSQLQueryBuilder.d.ts +192 -5
  7. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
  9. package/dist/core/Rovo.d.ts +116 -0
  10. package/dist/core/Rovo.d.ts.map +1 -0
  11. package/dist/core/Rovo.js +647 -0
  12. package/dist/core/Rovo.js.map +1 -0
  13. package/dist/utils/forgeDriver.d.ts +3 -2
  14. package/dist/utils/forgeDriver.d.ts.map +1 -1
  15. package/dist/utils/forgeDriver.js +20 -16
  16. package/dist/utils/forgeDriver.js.map +1 -1
  17. package/dist/utils/metadataContextUtils.d.ts +27 -1
  18. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  19. package/dist/utils/metadataContextUtils.js +215 -12
  20. package/dist/utils/metadataContextUtils.js.map +1 -1
  21. package/dist/webtriggers/index.d.ts +1 -0
  22. package/dist/webtriggers/index.d.ts.map +1 -1
  23. package/dist/webtriggers/index.js +1 -0
  24. package/dist/webtriggers/index.js.map +1 -1
  25. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +60 -0
  26. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
  27. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js +55 -0
  28. package/dist/webtriggers/topSlowestStatementLastHourTrigger.js.map +1 -0
  29. package/package.json +13 -11
  30. package/src/core/ForgeSQLORM.ts +142 -14
  31. package/src/core/ForgeSQLQueryBuilder.ts +213 -4
  32. package/src/core/Rovo.ts +765 -0
  33. package/src/utils/forgeDriver.ts +34 -19
  34. package/src/utils/metadataContextUtils.ts +267 -12
  35. package/src/webtriggers/index.ts +1 -0
  36. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +69 -0
@@ -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
  };
@@ -1,34 +1,289 @@
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 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
139
+ */
140
+ function formatExplainPlan(planRows: ExplainAnalyzeRow[]): string {
141
+ if (!planRows || planRows.length === 0) {
142
+ return "No execution plan available";
143
+ }
144
+
145
+ const lines: string[] = [];
146
+
147
+ for (const row of planRows) {
148
+ const parts: string[] = [];
149
+
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);
153
+
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(", ")}]`);
158
+
159
+ if (row.executionInfo) parts.push(`execution info:${row.executionInfo}`);
160
+
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(", ")})`);
165
+
166
+ if (row.accessObject) parts.push(`access object:${row.accessObject}`);
167
+
168
+ lines.push(parts.join(" | "));
169
+ }
170
+
171
+ return lines.join("\n");
172
+ }
173
+
174
+ /**
175
+ * 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
179
+ */
180
+ async function printPlansUsingSummaryTables(
181
+ context: MetadataQueryContext,
182
+ options: Required<MetadataQueryOptions>,
183
+ ): Promise<boolean> {
184
+ const timeDiff = Date.now() - context.beginTime.getTime();
185
+
186
+ if (options.mode !== "SummaryTable") {
187
+ return false;
188
+ }
189
+
190
+ if (timeDiff <= options.summaryTableWindowTime) {
191
+ await new Promise((resolve) => setTimeout(resolve, 200));
192
+ const summaryTableDiffMs = Date.now() - context.beginTime.getTime();
193
+ await printQueriesWithPlan(context.forgeSQLORM, summaryTableDiffMs);
194
+ return true;
195
+ }
196
+ // eslint-disable-next-line no-console
197
+ console.warn("Summary table window expired — showing query plans instead");
198
+ return false;
199
+ }
200
+
201
+ /**
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
206
+ */
207
+ async function printTopQueriesPlans(
208
+ context: MetadataQueryContext,
209
+ options: Required<MetadataQueryOptions>,
210
+ ): Promise<void> {
211
+ const topQueries = context.statistics
212
+ .sort((a, b) => b.metadata.dbExecutionTime - a.metadata.dbExecutionTime)
213
+ .slice(0, options.topQueries);
214
+
215
+ for (const query of topQueries) {
216
+ const normalizedQuery = options.normalizeQuery
217
+ ? normalizeSqlForLogging(query.query)
218
+ : query.query;
219
+ if (options.showSlowestPlans) {
220
+ const explainAnalyzeRows = await context.forgeSQLORM
221
+ .analyze()
222
+ .explainAnalyzeRaw(query.query, query.params);
223
+ const formattedPlan = formatExplainPlan(explainAnalyzeRows);
224
+ // eslint-disable-next-line no-console
225
+ console.warn(
226
+ `SQL: ${normalizedQuery} | Time: ${query.metadata.dbExecutionTime} ms\n Plan:\n${formattedPlan}`,
227
+ );
228
+ } else {
229
+ // eslint-disable-next-line no-console
230
+ console.warn(`SQL: ${normalizedQuery} | Time: ${query.metadata.dbExecutionTime} ms`);
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Saves query metadata to the current context and sets up the printQueriesWithPlan function.
237
+ * @param stringQuery - The SQL query string
238
+ * @param params - Query parameters
239
+ * @param metadata - Query execution metadata
240
+ */
241
+ export async function saveMetaDataToContext(
242
+ stringQuery: string,
243
+ params: unknown[],
244
+ metadata: ForgeSQLMetadata,
245
+ ): Promise<void> {
16
246
  const context = metadataQueryContext.getStore();
17
- if (context) {
18
- context.printQueriesWithPlan = async () => {
19
- if (process.env.NODE_ENV !== "test") {
20
- await new Promise((r) => setTimeout(r, 200));
21
- }
22
- await printQueriesWithPlan(context.forgeSQLORM, Date.now() - context.beginTime.getTime());
23
- };
24
- if (metadata) {
25
- context.totalResponseSize += metadata.responseSize;
26
- context.totalDbExecutionTime += metadata.dbExecutionTime;
247
+ if (!context) {
248
+ return;
249
+ }
250
+
251
+ // Initialize statistics array if needed
252
+ if (!context.statistics) {
253
+ context.statistics = [];
254
+ }
255
+
256
+ // Merge options with defaults
257
+ context.options = mergeOptionsWithDefaults(context.options);
258
+
259
+ // Add query statistics
260
+ context.statistics.push({ query: stringQuery, params, metadata });
261
+
262
+ // Set up printQueriesWithPlan function
263
+ context.printQueriesWithPlan = async () => {
264
+ 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;
27
270
  }
28
- // Log the results to console
271
+
272
+ // Fall back to printing top queries plans
273
+ await printTopQueriesPlans(context, options);
274
+ };
275
+
276
+ // Update aggregated metrics
277
+ if (metadata) {
278
+ context.totalResponseSize += metadata.responseSize;
279
+ context.totalDbExecutionTime += metadata.dbExecutionTime;
29
280
  }
30
281
  }
31
282
 
283
+ /**
284
+ * Gets the latest metadata from the current context.
285
+ * @returns The current metadata context or undefined if not in a context
286
+ */
32
287
  export async function getLastestMetadata(): Promise<MetadataQueryContext | undefined> {
33
288
  return metadataQueryContext.getStore();
34
289
  }
@@ -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
 
8
9
  export interface TriggerResponse<BODY> {
9
10
  body?: BODY;
@@ -0,0 +1,69 @@
1
+ import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
2
+ import { slowQuerySchedulerTrigger, TriggerResponse } from "./";
3
+ import { OperationType } from "../utils/requestTypeContextUtils";
4
+
5
+ export interface TriggerOptions {
6
+ warnThresholdMs?: number;
7
+ memoryThresholdBytes?: number;
8
+ showPlan?: boolean;
9
+ operationType?: OperationType;
10
+ topN?: number;
11
+ hours?: number;
12
+ tables?: "SUMMARY_AND_HISTORY" | "CLUSTER_SUMMARY_AND_HISTORY";
13
+ }
14
+
15
+ /**
16
+ * @deprecated This function is deprecated and will be removed in a future version.
17
+ *
18
+ * This function was previously a complex implementation that directly queried
19
+ * CLUSTER_STATEMENTS_SUMMARY tables to analyze query performance. However, this approach
20
+ * had reliability issues with long-running functions where metadata could be evicted
21
+ * before the function completes.
22
+ *
23
+ * The recommended replacement is to use the new observability system with `executeWithMetadata`:
24
+ * - **TopSlowest mode** (default): Deterministic logging of SQL digests executed in resolvers
25
+ * - **SummaryTable mode** (optional): Uses CLUSTER_STATEMENTS_SUMMARY with a short memory window
26
+ * - Automatic fallback mechanisms for long-running functions
27
+ * - More reliable post-mortem diagnostics for Timeout and OOM errors
28
+ *
29
+ * Note: `slowQuerySchedulerTrigger` is a different function that analyzes TiDB's slow query log
30
+ * and is not a direct replacement for this function.
31
+ *
32
+ * For more details on the improvements and migration path, see:
33
+ * https://community.developer.atlassian.com/t/practical-sql-observability-for-forge-apps-with-forge-sql-orm/123456
34
+ *
35
+ * @param orm - ForgeSQL ORM instance
36
+ * @param options - Configuration options (currently passed to slowQuerySchedulerTrigger as a temporary wrapper)
37
+ * @returns Promise<TriggerResponse<string>> - HTTP response with query results or error
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // Old usage (deprecated):
42
+ * await topSlowestStatementLastHourTrigger(forgeSQL, { hours: 1 });
43
+ *
44
+ * // New usage (recommended - use executeWithMetadata in your resolvers):
45
+ * await forgeSQL.executeWithMetadata(
46
+ * async () => {
47
+ * // your resolver logic
48
+ * },
49
+ * async (totalDbTime, totalResponseSize, printPlan) => {
50
+ * // custom observability logic
51
+ * if (totalDbTime > 1000) await printPlan();
52
+ * },
53
+ * {
54
+ * mode: "TopSlowest",
55
+ * topQueries: 1,
56
+ * showSlowestPlans: true
57
+ * }
58
+ * );
59
+ * ```
60
+ */
61
+ export const topSlowestStatementLastHourTrigger = async (
62
+ orm: ForgeSqlOperation,
63
+ options?: TriggerOptions,
64
+ ): Promise<TriggerResponse<string>> => {
65
+ return slowQuerySchedulerTrigger(
66
+ orm,
67
+ options ? { timeout: 3000, hours: options.hours ?? 1 } : { timeout: 3000, hours: 1 },
68
+ );
69
+ };