forge-sql-orm 2.1.2 → 2.1.4

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 (38) hide show
  1. package/README.md +205 -0
  2. package/dist/ForgeSQLORM.js +677 -56
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +680 -59
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLQueryBuilder.d.ts +1 -2
  7. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  8. package/dist/core/SystemTables.d.ts +5039 -0
  9. package/dist/core/SystemTables.d.ts.map +1 -1
  10. package/dist/utils/cacheUtils.d.ts.map +1 -1
  11. package/dist/utils/forgeDriver.d.ts.map +1 -1
  12. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  13. package/dist/utils/sqlUtils.d.ts +1 -1
  14. package/dist/utils/sqlUtils.d.ts.map +1 -1
  15. package/dist/webtriggers/applyMigrationsWebTrigger.d.ts +1 -1
  16. package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
  17. package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
  18. package/dist/webtriggers/dropTablesMigrationWebTrigger.d.ts.map +1 -1
  19. package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -1
  20. package/dist/webtriggers/index.d.ts +1 -0
  21. package/dist/webtriggers/index.d.ts.map +1 -1
  22. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +72 -0
  23. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
  24. package/package.json +8 -7
  25. package/src/core/ForgeSQLQueryBuilder.ts +13 -9
  26. package/src/core/SystemTables.ts +313 -1
  27. package/src/lib/drizzle/extensions/additionalActions.ts +2 -2
  28. package/src/utils/cacheContextUtils.ts +2 -2
  29. package/src/utils/cacheUtils.ts +3 -1
  30. package/src/utils/forgeDriver.ts +16 -21
  31. package/src/utils/forgeDriverProxy.ts +10 -3
  32. package/src/utils/sqlUtils.ts +32 -7
  33. package/src/webtriggers/applyMigrationsWebTrigger.ts +21 -15
  34. package/src/webtriggers/dropMigrationWebTrigger.ts +8 -4
  35. package/src/webtriggers/dropTablesMigrationWebTrigger.ts +8 -4
  36. package/src/webtriggers/fetchSchemaWebTrigger.ts +7 -3
  37. package/src/webtriggers/index.ts +1 -0
  38. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +305 -0
@@ -0,0 +1,305 @@
1
+ import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
2
+ import { clusterStatementsSummary, clusterStatementsSummaryHistory } from "../core/SystemTables";
3
+ import { desc, gte, sql } from "drizzle-orm";
4
+ import { unionAll } from "drizzle-orm/mysql-core";
5
+ import { formatLimitOffset } from "../utils/sqlUtils";
6
+
7
+ /**
8
+ * Scheduler trigger: log and return the single slowest statement from the last hour, filtered by latency OR memory usage.
9
+ *
10
+ * When scheduled (e.g. hourly), this trigger queries
11
+ * INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY for the last hour
12
+ * and prints the TOP 1 entry (by AVG_LATENCY) if it exceeds either threshold.
13
+ *
14
+ * **OR Logic**: Statements are included if they exceed EITHER threshold:
15
+ * - avgLatencyMs > warnThresholdMs OR
16
+ * - avgMemBytes > memoryThresholdBytes
17
+ *
18
+ * **Pro Tips:**
19
+ * - Memory-only monitoring: Set warnThresholdMs to 10000ms (effectively disabled)
20
+ * - Latency-only monitoring: Set memoryThresholdBytes to 16MB (16 * 1024 * 1024) (effectively disabled)
21
+ * - Combined monitoring: Use both thresholds for comprehensive monitoring
22
+ *
23
+ * Excludes statements with empty `digestText`, empty `digest`, or service statements (`Use`, `Set`, `Show`).
24
+ *
25
+ * Logging rule:
26
+ * - Query exceeds warnThresholdMs OR memoryThresholdBytes → console.warn (logged)
27
+ * - otherwise → not logged
28
+ *
29
+ * @param orm ForgeSQL ORM instance (required)
30
+ * @param warnThresholdMs Milliseconds threshold for logging and filtering (default: 300ms)
31
+ * @param memoryThresholdBytes Bytes threshold for average memory usage (default: 8MB)
32
+ * @returns HTTP response with a JSON payload containing the filtered rows
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import ForgeSQL, { topSlowestStatementLastHourTrigger } from "forge-sql-orm";
37
+ *
38
+ * const FORGE_SQL_ORM = new ForgeSQL();
39
+ *
40
+ * // Default thresholds: 300ms latency OR 8MB memory
41
+ * export const topSlowQueryTrigger = () =>
42
+ * topSlowestStatementLastHourTrigger(FORGE_SQL_ORM);
43
+ *
44
+ * // Only latency monitoring: 500ms threshold (memory effectively disabled)
45
+ * export const latencyOnlyTrigger = () =>
46
+ * topSlowestStatementLastHourTrigger(FORGE_SQL_ORM, 500, 16 * 1024 * 1024);
47
+ *
48
+ * // Only memory monitoring: 4MB threshold (latency effectively disabled)
49
+ * export const memoryOnlyTrigger = () =>
50
+ * topSlowestStatementLastHourTrigger(FORGE_SQL_ORM, 10000, 4 * 1024 * 1024);
51
+ *
52
+ * // Both thresholds: 500ms latency OR 8MB memory
53
+ * export const bothThresholdsTrigger = () =>
54
+ * topSlowestStatementLastHourTrigger(FORGE_SQL_ORM, 500, 8 * 1024 * 1024);
55
+ * ```
56
+ *
57
+ * @example
58
+ * ```yaml
59
+ * scheduledTrigger:
60
+ * - key: top-slow-query-trigger
61
+ * function: topSlowQueryTrigger
62
+ * interval: hour
63
+ *
64
+ * function:
65
+ * - key: topSlowQueryTrigger
66
+ * handler: index.topSlowQueryTrigger
67
+ * ```
68
+ */
69
+ // Main scheduler trigger function to log the single slowest SQL statement from the last hour.
70
+ export const topSlowestStatementLastHourTrigger = async (
71
+ orm: ForgeSqlOperation,
72
+ // warnThresholdMs: Only log queries whose average latency (ms) exceeds this threshold (default: 300ms)
73
+ warnThresholdMs: number = 300,
74
+ // memoryThresholdBytes: Also include queries whose avg memory usage exceeds this threshold (default: 8MB)
75
+ memoryThresholdBytes: number = 8 * 1024 * 1024,
76
+ ) => {
77
+ // Helper: Convert nanoseconds to milliseconds (for latency fields)
78
+ const nsToMs = (v: unknown) => {
79
+ const n = Number(v);
80
+ return Number.isFinite(n) ? n / 1e6 : NaN;
81
+ };
82
+
83
+ // Helper: Convert bytes to megabytes (for memory fields)
84
+ const bytesToMB = (v: unknown) => {
85
+ const n = Number(v);
86
+ return Number.isFinite(n) ? n / (1024 * 1024) : NaN;
87
+ };
88
+
89
+ // Helper: JSON.stringify replacer to handle BigInt values (so BigInt serializes as string)
90
+ const jsonSafeStringify = (value: unknown) =>
91
+ JSON.stringify(value, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
92
+
93
+ // Number of top slow queries to fetch
94
+ const TOP_N = 1;
95
+
96
+ try {
97
+ // Get references to system summary tables
98
+ const summaryHistory = clusterStatementsSummaryHistory;
99
+ const summary = clusterStatementsSummary;
100
+ // Helper to define the selected fields (selection shape) for both tables
101
+ const selectShape = (t: typeof summaryHistory | typeof summary) => ({
102
+ digest: t.digest,
103
+ stmtType: t.stmtType,
104
+ schemaName: t.schemaName,
105
+ execCount: t.execCount,
106
+
107
+ avgLatencyNs: t.avgLatency,
108
+ maxLatencyNs: t.maxLatency,
109
+ minLatencyNs: t.minLatency,
110
+
111
+ avgProcessTimeNs: t.avgProcessTime,
112
+ avgWaitTimeNs: t.avgWaitTime,
113
+ avgBackoffTimeNs: t.avgBackoffTime,
114
+
115
+ avgTotalKeys: t.avgTotalKeys,
116
+ firstSeen: t.firstSeen,
117
+ lastSeen: t.lastSeen,
118
+ planInCache: t.planInCache,
119
+ planCacheHits: t.planCacheHits,
120
+ digestText: t.digestText,
121
+ plan: t.plan,
122
+ avgMemBytes: (t as any).avgMem,
123
+ maxMemBytes: (t as any).maxMem,
124
+ });
125
+
126
+ // Filters: Only include rows from the last hour for each table
127
+ const lastHourFilterHistory = gte(
128
+ summaryHistory.summaryEndTime,
129
+ sql`DATE_SUB(NOW(), INTERVAL 1 HOUR)`,
130
+ );
131
+ const lastHourFilterSummary = gte(
132
+ summary.summaryEndTime,
133
+ sql`DATE_SUB(NOW(), INTERVAL 1 HOUR)`,
134
+ );
135
+
136
+ // Query for summary history table (last hour)
137
+ const qHistory = orm
138
+ .getDrizzleQueryBuilder()
139
+ .select(selectShape(summaryHistory))
140
+ .from(summaryHistory)
141
+ .where(lastHourFilterHistory);
142
+
143
+ // Query for summary table (last hour)
144
+ const qSummary = orm
145
+ .getDrizzleQueryBuilder()
146
+ .select(selectShape(summary))
147
+ .from(summary)
148
+ .where(lastHourFilterSummary);
149
+
150
+ // Use UNION ALL to combine results from both tables (avoids duplicates, keeps all rows)
151
+ // This is necessary because some statements may only be present in one of the tables.
152
+ const combined = unionAll(qHistory, qSummary).as("combined");
153
+
154
+ // Threshold in nanoseconds (warnThresholdMs → ns)
155
+ const thresholdNs = Math.floor(warnThresholdMs * 1e6);
156
+ // memoryThresholdBytes is already provided in bytes (default 8MB)
157
+
158
+ // Group duplicates by digest+stmtType+schemaName and aggregate metrics
159
+ const grouped = orm
160
+ .getDrizzleQueryBuilder()
161
+ .select({
162
+ digest: combined.digest,
163
+ stmtType: combined.stmtType,
164
+ schemaName: combined.schemaName,
165
+ execCount: sql<number>`SUM(${combined.execCount})`.as("execCount"),
166
+
167
+ avgLatencyNs: sql<number>`MAX(${combined.avgLatencyNs})`.as("avgLatencyNs"),
168
+ maxLatencyNs: sql<number>`MAX(${combined.maxLatencyNs})`.as("maxLatencyNs"),
169
+ minLatencyNs: sql<number>`MIN(${combined.minLatencyNs})`.as("minLatencyNs"),
170
+
171
+ avgProcessTimeNs: sql<number>`MAX(${combined.avgProcessTimeNs})`.as("avgProcessTimeNs"),
172
+ avgWaitTimeNs: sql<number>`MAX(${combined.avgWaitTimeNs})`.as("avgWaitTimeNs"),
173
+ avgBackoffTimeNs: sql<number>`MAX(${combined.avgBackoffTimeNs})`.as("avgBackoffTimeNs"),
174
+
175
+ avgMemBytes: sql<number>`MAX(${combined.avgMemBytes})`.as("avgMemBytes"),
176
+ maxMemBytes: sql<number>`MAX(${combined.maxMemBytes})`.as("maxMemBytes"),
177
+
178
+ avgTotalKeys: sql<number>`MAX(${combined.avgTotalKeys})`.as("avgTotalKeys"),
179
+ firstSeen: sql<string>`MIN(${combined.firstSeen})`.as("firstSeen"),
180
+ lastSeen: sql<string>`MAX(${combined.lastSeen})`.as("lastSeen"),
181
+ planInCache: sql<boolean>`MAX(${combined.planInCache})`.as("planInCache"),
182
+ planCacheHits: sql<number>`SUM(${combined.planCacheHits})`.as("planCacheHits"),
183
+ // Prefer a non-empty sample text/plan via MAX; acceptable for de-dup
184
+ digestText: sql<string>`MAX(${combined.digestText})`.as("digestText"),
185
+ plan: sql<string>`MAX(${combined.plan})`.as("plan"),
186
+ })
187
+ .from(combined)
188
+ .where(
189
+ sql`COALESCE(${combined.digest}, '') <> '' AND COALESCE(${combined.digestText}, '') <> '' AND COALESCE(${combined.stmtType}, '') NOT IN ('Use','Set','Show')`,
190
+ )
191
+ .groupBy(combined.digest, combined.stmtType, combined.schemaName)
192
+ .as("grouped");
193
+
194
+ // Final selection: filter by threshold, sort by avg latency desc, limit TOP_N
195
+ const rows = await orm
196
+ .getDrizzleQueryBuilder()
197
+ .select({
198
+ digest: grouped.digest,
199
+ stmtType: grouped.stmtType,
200
+ schemaName: grouped.schemaName,
201
+ execCount: grouped.execCount,
202
+
203
+ avgLatencyNs: grouped.avgLatencyNs,
204
+ maxLatencyNs: grouped.maxLatencyNs,
205
+ minLatencyNs: grouped.minLatencyNs,
206
+
207
+ avgProcessTimeNs: grouped.avgProcessTimeNs,
208
+ avgWaitTimeNs: grouped.avgWaitTimeNs,
209
+ avgBackoffTimeNs: grouped.avgBackoffTimeNs,
210
+
211
+ avgMemBytes: grouped.avgMemBytes,
212
+ maxMemBytes: grouped.maxMemBytes,
213
+
214
+ avgTotalKeys: grouped.avgTotalKeys,
215
+ firstSeen: grouped.firstSeen,
216
+ lastSeen: grouped.lastSeen,
217
+ planInCache: grouped.planInCache,
218
+ planCacheHits: grouped.planCacheHits,
219
+ digestText: grouped.digestText,
220
+ plan: grouped.plan,
221
+ })
222
+ .from(grouped)
223
+ .where(
224
+ sql`${grouped.avgLatencyNs} > ${thresholdNs} OR ${grouped.avgMemBytes} > ${memoryThresholdBytes}`,
225
+ )
226
+ .orderBy(desc(grouped.avgLatencyNs))
227
+ .limit(formatLimitOffset(TOP_N));
228
+
229
+ // Map each row into a formatted object with ms and rank, for easier consumption/logging
230
+ const formatted = rows.map((r, i) => ({
231
+ rank: i + 1, // 1-based rank in the top N
232
+ digest: r.digest,
233
+ stmtType: r.stmtType,
234
+ schemaName: r.schemaName,
235
+ execCount: r.execCount,
236
+ avgLatencyMs: nsToMs(r.avgLatencyNs), // Convert ns to ms for readability
237
+ maxLatencyMs: nsToMs(r.maxLatencyNs),
238
+ minLatencyMs: nsToMs(r.minLatencyNs),
239
+ avgProcessTimeMs: nsToMs(r.avgProcessTimeNs),
240
+ avgWaitTimeMs: nsToMs(r.avgWaitTimeNs),
241
+ avgBackoffTimeMs: nsToMs(r.avgBackoffTimeNs),
242
+ avgMemMB: bytesToMB(r.avgMemBytes),
243
+ maxMemMB: bytesToMB(r.maxMemBytes),
244
+ avgMemBytes: r.avgMemBytes,
245
+ maxMemBytes: r.maxMemBytes,
246
+ avgTotalKeys: r.avgTotalKeys,
247
+ firstSeen: r.firstSeen,
248
+ lastSeen: r.lastSeen,
249
+ planInCache: r.planInCache,
250
+ planCacheHits: r.planCacheHits,
251
+ digestText: r.digestText,
252
+ plan: r.plan,
253
+ }));
254
+
255
+ // Log each entry (SQL already filtered by threshold)
256
+ for (const f of formatted) {
257
+ // eslint-disable-next-line no-console
258
+ console.warn(
259
+ `${f.rank}. ${f.stmtType} avg=${f.avgLatencyMs?.toFixed?.(2)}ms max=${f.maxLatencyMs?.toFixed?.(2)}ms mem≈${f.avgMemMB?.toFixed?.(2)}MB(max ${f.maxMemMB?.toFixed?.(2)}MB) exec=${f.execCount} \n` +
260
+ ` digest=${f.digest}\n` +
261
+ ` sql=${(f.digestText || "").slice(0, 300)}${f.digestText && f.digestText.length > 300 ? "…" : ""}`,
262
+ );
263
+ if (f.plan) {
264
+ // print full plan separately (not truncated)
265
+ // eslint-disable-next-line no-console
266
+ console.warn(` full plan:\n${f.plan}`);
267
+ }
268
+ }
269
+
270
+ // Return HTTP response with JSON payload of the results
271
+ return {
272
+ headers: { "Content-Type": ["application/json"] },
273
+ statusCode: 200,
274
+ statusText: "OK",
275
+ body: jsonSafeStringify({
276
+ success: true,
277
+ window: "last_1h",
278
+ top: TOP_N,
279
+ warnThresholdMs,
280
+ memoryThresholdBytes,
281
+ rows: formatted,
282
+ generatedAt: new Date().toISOString(),
283
+ }),
284
+ };
285
+ } catch (error: any) {
286
+ // Catch any error (DB, logic, etc) and log with details for debugging
287
+ // This ensures the scheduler never crashes and always returns a response.
288
+ // eslint-disable-next-line no-console
289
+ console.error(
290
+ "Error in topSlowestStatementLastHourTrigger:",
291
+ error?.cause?.context?.debug?.sqlMessage ?? error?.cause ?? error,
292
+ );
293
+ return {
294
+ headers: { "Content-Type": ["application/json"] },
295
+ statusCode: 500,
296
+ statusText: "Internal Server Error",
297
+ body: jsonSafeStringify({
298
+ success: false,
299
+ message: "Failed to fetch or log slow queries",
300
+ error: error?.cause?.context?.debug?.sqlMessage ?? error?.cause?.message,
301
+ timestamp: new Date().toISOString(),
302
+ }),
303
+ };
304
+ }
305
+ };