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
@@ -2,6 +2,7 @@ import {
2
2
  and,
3
3
  AnyColumn,
4
4
  Column,
5
+ desc,
5
6
  gte,
6
7
  ilike,
7
8
  isNotNull,
@@ -100,37 +101,37 @@ export const parseDateTime = (value: string | Date, format: string): Date => {
100
101
  };
101
102
 
102
103
  /**
103
- * Helper function to validate and format a date-like value using Luxon DateTime.
104
- * @param value - Date object, ISO/RFC2822/SQL/HTTP string, or timestamp (number|string).
105
- * @param format - DateTime format string (Luxon format tokens).
106
- * @returns Formatted date string.
107
- * @throws Error if value cannot be parsed as a valid date.
104
+ * Parses a string value into DateTime using multiple format parsers
108
105
  */
109
- export function formatDateTime(
110
- value: Date | string | number,
111
- format: string,
112
- isTimeStamp: boolean,
113
- ): string {
106
+ function parseStringToDateTime(value: string): DateTime | null {
107
+ const parsers = [DateTime.fromISO, DateTime.fromRFC2822, DateTime.fromSQL, DateTime.fromHTTP];
108
+
109
+ for (const parser of parsers) {
110
+ const dt = parser(value);
111
+ if (dt.isValid) {
112
+ return dt;
113
+ }
114
+ }
115
+
116
+ // Try parsing as number string
117
+ const parsed = Number(value);
118
+ if (!Number.isNaN(parsed)) {
119
+ return DateTime.fromMillis(parsed);
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Converts a value to DateTime
127
+ */
128
+ function valueToDateTime(value: Date | string | number): DateTime {
114
129
  let dt: DateTime | null = null;
115
130
 
116
131
  if (value instanceof Date) {
117
132
  dt = DateTime.fromJSDate(value);
118
133
  } else if (typeof value === "string") {
119
- for (const parser of [
120
- DateTime.fromISO,
121
- DateTime.fromRFC2822,
122
- DateTime.fromSQL,
123
- DateTime.fromHTTP,
124
- ]) {
125
- dt = parser(value);
126
- if (dt.isValid) break;
127
- }
128
- if (!dt?.isValid) {
129
- const parsed = Number(value);
130
- if (!Number.isNaN(parsed)) {
131
- dt = DateTime.fromMillis(parsed);
132
- }
133
- }
134
+ dt = parseStringToDateTime(value);
134
135
  } else if (typeof value === "number") {
135
136
  dt = DateTime.fromMillis(value);
136
137
  } else {
@@ -140,20 +141,47 @@ export function formatDateTime(
140
141
  if (!dt?.isValid) {
141
142
  throw new Error("Invalid Date");
142
143
  }
144
+
145
+ return dt;
146
+ }
147
+
148
+ /**
149
+ * Validates timestamp range for Atlassian Forge compatibility
150
+ */
151
+ function validateTimestampRange(dt: DateTime): void {
143
152
  const minDate = DateTime.fromSeconds(1);
144
153
  const maxDate = DateTime.fromMillis(2147483647 * 1000); // 2038-01-19 03:14:07.999 UTC
145
154
 
155
+ if (dt < minDate) {
156
+ throw new Error(
157
+ "Atlassian Forge does not support zero or negative timestamps. Allowed range: from '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999'.",
158
+ );
159
+ }
160
+
161
+ if (dt > maxDate) {
162
+ throw new Error(
163
+ "Atlassian Forge does not support timestamps beyond 2038-01-19 03:14:07.999999. Please use a smaller date within the supported range.",
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Helper function to validate and format a date-like value using Luxon DateTime.
170
+ * @param value - Date object, ISO/RFC2822/SQL/HTTP string, or timestamp (number|string).
171
+ * @param format - DateTime format string (Luxon format tokens).
172
+ * @param isTimeStamp - Whether to validate timestamp range
173
+ * @returns Formatted date string.
174
+ * @throws Error if value cannot be parsed as a valid date.
175
+ */
176
+ export function formatDateTime(
177
+ value: Date | string | number,
178
+ format: string,
179
+ isTimeStamp: boolean,
180
+ ): string {
181
+ const dt = valueToDateTime(value);
182
+
146
183
  if (isTimeStamp) {
147
- if (dt < minDate) {
148
- throw new Error(
149
- "Atlassian Forge does not support zero or negative timestamps. Allowed range: from '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999'.",
150
- );
151
- }
152
- if (dt > maxDate) {
153
- throw new Error(
154
- "Atlassian Forge does not support timestamps beyond 2038-01-19 03:14:07.999999. Please use a smaller date within the supported range.",
155
- );
156
- }
184
+ validateTimestampRange(dt);
157
185
  }
158
186
 
159
187
  return dt.toFormat(format);
@@ -169,10 +197,7 @@ export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyC
169
197
  const { columns, primaryKeys } = getTableMetadata(table);
170
198
 
171
199
  // First try to find primary keys in columns
172
- const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
173
- string,
174
- AnyColumn,
175
- ][];
200
+ const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
176
201
 
177
202
  if (columnPrimaryKeys.length > 0) {
178
203
  return columnPrimaryKeys;
@@ -199,6 +224,85 @@ export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyC
199
224
  return [];
200
225
  }
201
226
 
227
+ /**
228
+ * Processes foreign keys from foreignKeysSymbol
229
+ */
230
+ function processForeignKeysFromSymbol(
231
+ table: AnyMySqlTable,
232
+ foreignKeysSymbol: symbol,
233
+ ): ForeignKeyBuilder[] {
234
+ const foreignKeys: ForeignKeyBuilder[] = [];
235
+ // @ts-ignore
236
+ const fkArray: any[] = table[foreignKeysSymbol];
237
+
238
+ if (!fkArray) {
239
+ return foreignKeys;
240
+ }
241
+
242
+ for (const fk of fkArray) {
243
+ if (fk.reference) {
244
+ const item = fk.reference(fk);
245
+ foreignKeys.push(item);
246
+ }
247
+ }
248
+
249
+ return foreignKeys;
250
+ }
251
+
252
+ /**
253
+ * Extracts config builders from config builder data
254
+ */
255
+ function extractConfigBuilders(configBuilderData: any): any[] {
256
+ if (Array.isArray(configBuilderData)) {
257
+ return configBuilderData;
258
+ }
259
+
260
+ return Object.values(configBuilderData).map((item) => (item as ConfigBuilderData).value ?? item);
261
+ }
262
+
263
+ /**
264
+ * Checks if a builder is a ForeignKeyBuilder
265
+ */
266
+ function isForeignKeyBuilder(builder: any): boolean {
267
+ if (!builder?.constructor) {
268
+ return false;
269
+ }
270
+
271
+ const builderName = builder.constructor.name.toLowerCase();
272
+ return builderName.includes("foreignkeybuilder");
273
+ }
274
+
275
+ /**
276
+ * Processes foreign keys from extraSymbol
277
+ */
278
+ function processForeignKeysFromExtra(
279
+ table: AnyMySqlTable,
280
+ extraSymbol: symbol,
281
+ ): ForeignKeyBuilder[] {
282
+ const foreignKeys: ForeignKeyBuilder[] = [];
283
+ // @ts-ignore
284
+ const extraConfigBuilder = table[extraSymbol];
285
+
286
+ if (!extraConfigBuilder || typeof extraConfigBuilder !== "function") {
287
+ return foreignKeys;
288
+ }
289
+
290
+ const configBuilderData = extraConfigBuilder(table);
291
+ if (!configBuilderData) {
292
+ return foreignKeys;
293
+ }
294
+
295
+ const configBuilders = extractConfigBuilders(configBuilderData);
296
+
297
+ for (const builder of configBuilders) {
298
+ if (isForeignKeyBuilder(builder)) {
299
+ foreignKeys.push(builder);
300
+ }
301
+ }
302
+
303
+ return foreignKeys;
304
+ }
305
+
202
306
  /**
203
307
  * Processes foreign keys from both foreignKeysSymbol and extraSymbol
204
308
  * @param table - The table schema
@@ -215,57 +319,117 @@ function processForeignKeys(
215
319
 
216
320
  // Process foreign keys from foreignKeysSymbol
217
321
  if (foreignKeysSymbol) {
218
- // @ts-ignore
219
- const fkArray: any[] = table[foreignKeysSymbol];
220
- if (fkArray) {
221
- for (const fk of fkArray) {
222
- if (fk.reference) {
223
- const item = fk.reference(fk);
224
- foreignKeys.push(item);
225
- }
226
- }
227
- }
322
+ foreignKeys.push(...processForeignKeysFromSymbol(table, foreignKeysSymbol));
228
323
  }
229
324
 
230
325
  // Process foreign keys from extraSymbol
231
326
  if (extraSymbol) {
232
- // @ts-ignore
233
- const extraConfigBuilder = table[extraSymbol];
234
- if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
235
- const configBuilderData = extraConfigBuilder(table);
236
- if (configBuilderData) {
237
- const configBuilders = Array.isArray(configBuilderData)
238
- ? configBuilderData
239
- : Object.values(configBuilderData).map(
240
- (item) => (item as ConfigBuilderData).value ?? item,
241
- );
242
-
243
- for (const builder of configBuilders) {
244
- if (!builder?.constructor) continue;
245
-
246
- const builderName = builder.constructor.name.toLowerCase();
247
- if (builderName.includes("foreignkeybuilder")) {
248
- foreignKeys.push(builder);
249
- }
250
- }
251
- }
252
- }
327
+ foreignKeys.push(...processForeignKeysFromExtra(table, extraSymbol));
253
328
  }
254
329
 
255
330
  return foreignKeys;
256
331
  }
257
332
 
333
+ /**
334
+ * Extracts symbols from table schema.
335
+ * @param table - The table schema
336
+ * @returns Object containing relevant symbols
337
+ */
338
+ function extractTableSymbols(table: AnyMySqlTable) {
339
+ const symbols = Object.getOwnPropertySymbols(table);
340
+ return {
341
+ nameSymbol: symbols.find((s) => s.toString().includes("Name")),
342
+ columnsSymbol: symbols.find((s) => s.toString().includes("Columns")),
343
+ foreignKeysSymbol: symbols.find((s) => s.toString().includes("ForeignKeys)")),
344
+ extraSymbol: symbols.find((s) => s.toString().includes("ExtraConfigBuilder")),
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Maps builder to appropriate array based on its type.
350
+ * @param builder - The builder object
351
+ * @param builders - The builders object containing all arrays
352
+ * @returns True if builder was added to a specific array, false otherwise
353
+ */
354
+ function addBuilderToTypedArray(
355
+ builder: any,
356
+ builders: {
357
+ indexes: AnyIndexBuilder[];
358
+ checks: CheckBuilder[];
359
+ primaryKeys: PrimaryKeyBuilder[];
360
+ uniqueConstraints: UniqueConstraintBuilder[];
361
+ },
362
+ ): boolean {
363
+ if (!builder?.constructor) {
364
+ return false;
365
+ }
366
+
367
+ const builderName = builder.constructor.name.toLowerCase();
368
+ const builderMap = {
369
+ indexbuilder: builders.indexes,
370
+ checkbuilder: builders.checks,
371
+ primarykeybuilder: builders.primaryKeys,
372
+ uniqueconstraintbuilder: builders.uniqueConstraints,
373
+ };
374
+
375
+ for (const [type, array] of Object.entries(builderMap)) {
376
+ if (builderName.includes(type)) {
377
+ array.push(builder);
378
+ return true;
379
+ }
380
+ }
381
+
382
+ return false;
383
+ }
384
+
385
+ /**
386
+ * Processes extra configuration builders and adds them to the builders object.
387
+ * @param table - The table schema
388
+ * @param extraSymbol - The extra symbol from table
389
+ * @param builders - The builders object to populate
390
+ */
391
+ function processExtraConfigBuilders(
392
+ table: AnyMySqlTable,
393
+ extraSymbol: symbol | undefined,
394
+ builders: {
395
+ indexes: AnyIndexBuilder[];
396
+ checks: CheckBuilder[];
397
+ foreignKeys: ForeignKeyBuilder[];
398
+ primaryKeys: PrimaryKeyBuilder[];
399
+ uniqueConstraints: UniqueConstraintBuilder[];
400
+ extras: any[];
401
+ },
402
+ ): void {
403
+ if (!extraSymbol) {
404
+ return;
405
+ }
406
+
407
+ // @ts-ignore
408
+ const extraConfigBuilder = table[extraSymbol];
409
+ if (!extraConfigBuilder || typeof extraConfigBuilder !== "function") {
410
+ return;
411
+ }
412
+
413
+ const configBuilderData = extraConfigBuilder(table);
414
+ if (!configBuilderData) {
415
+ return;
416
+ }
417
+
418
+ const configBuilders = extractConfigBuilders(configBuilderData);
419
+
420
+ for (const builder of configBuilders) {
421
+ addBuilderToTypedArray(builder, builders);
422
+ builders.extras.push(builder);
423
+ }
424
+ }
425
+
258
426
  /**
259
427
  * Extracts table metadata from the schema.
260
428
  * @param {AnyMySqlTable} table - The table schema
261
429
  * @returns {MetadataInfo} Object containing table metadata
262
430
  */
263
431
  export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
264
- const symbols = Object.getOwnPropertySymbols(table);
265
- const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
266
- const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
267
- const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
268
- const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
432
+ const { nameSymbol, columnsSymbol, foreignKeysSymbol, extraSymbol } = extractTableSymbols(table);
269
433
 
270
434
  // Initialize builders arrays
271
435
  const builders = {
@@ -281,47 +445,7 @@ export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
281
445
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
282
446
 
283
447
  // Process extra configuration if available
284
- if (extraSymbol) {
285
- // @ts-ignore
286
- const extraConfigBuilder = table[extraSymbol];
287
- if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
288
- const configBuilderData = extraConfigBuilder(table);
289
- if (configBuilderData) {
290
- // Convert configBuilderData to array if it's an object
291
- const configBuilders = Array.isArray(configBuilderData)
292
- ? configBuilderData
293
- : Object.values(configBuilderData).map(
294
- (item) => (item as ConfigBuilderData).value ?? item,
295
- );
296
-
297
- // Process each builder
298
- for (const builder of configBuilders) {
299
- if (!builder?.constructor) continue;
300
-
301
- const builderName = builder.constructor.name.toLowerCase();
302
-
303
- // Map builder types to their corresponding arrays
304
- const builderMap = {
305
- indexbuilder: builders.indexes,
306
- checkbuilder: builders.checks,
307
- primarykeybuilder: builders.primaryKeys,
308
- uniqueconstraintbuilder: builders.uniqueConstraints,
309
- };
310
-
311
- // Add builder to appropriate array if it matches any type
312
- for (const [type, array] of Object.entries(builderMap)) {
313
- if (builderName.includes(type)) {
314
- array.push(builder);
315
- break;
316
- }
317
- }
318
-
319
- // Always add to extras array
320
- builders.extras.push(builder);
321
- }
322
- }
323
- }
324
- }
448
+ processExtraConfigBuilders(table, extraSymbol, builders);
325
449
 
326
450
  return {
327
451
  tableName: nameSymbol ? (table as any)[nameSymbol] : "",
@@ -372,7 +496,7 @@ function mapSelectTableToAlias(
372
496
  const { columns, tableName } = getTableMetadata(table);
373
497
  const selectionsTableFields: Record<string, unknown> = {};
374
498
  for (const name of Object.keys(columns)) {
375
- const column = columns[name] as AnyColumn;
499
+ const column = columns[name];
376
500
  const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
377
501
  const fieldAlias = sql.raw(uniqName);
378
502
  selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
@@ -419,38 +543,71 @@ export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
419
543
  }
420
544
  const aliasMap: AliasColumnMap = {};
421
545
  const selections: any = {};
422
- for (let i = 0; i < Object.entries(fields).length; i++) {
423
- const [name, fields1] = Object.entries(fields)[i];
546
+ for (const [name, fields1] of Object.entries(fields)) {
424
547
  mapSelectAllFieldsToAlias(selections, name, name, fields1, aliasMap);
425
548
  }
426
549
  return { selections, aliasMap };
427
550
  }
428
551
 
429
- function getAliasFromDrizzleAlias(value: unknown): string | undefined {
430
- const isSQL =
431
- value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
432
- if (isSQL) {
433
- const sql = value as SQL;
434
- const queryChunks = sql.queryChunks;
435
- if (queryChunks.length > 3) {
436
- const aliasNameChunk = queryChunks[queryChunks.length - 2];
437
- if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
438
- const aliasNameChunkSql = aliasNameChunk as SQL;
439
- if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
440
- const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
441
- if ("value" in queryChunksStringChunc) {
442
- const values = (queryChunksStringChunc as StringChunk).value;
443
- if (values && values.length === 1) {
444
- return values[0];
445
- }
446
- }
447
- }
448
- }
449
- }
552
+ /**
553
+ * Checks if value is a SQL object with queryChunks
554
+ */
555
+ function isSQLValue(value: unknown): value is SQL {
556
+ return (
557
+ value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value
558
+ );
559
+ }
560
+
561
+ /**
562
+ * Extracts the alias name chunk from query chunks if it exists and is a SQL object
563
+ */
564
+ function getAliasNameChunk(queryChunks: any[]): SQL | undefined {
565
+ if (queryChunks.length <= 3) {
566
+ return undefined;
567
+ }
568
+
569
+ const aliasNameChunk = queryChunks.at(-2);
570
+ if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
571
+ return aliasNameChunk as SQL;
450
572
  }
573
+
574
+ return undefined;
575
+ }
576
+
577
+ /**
578
+ * Extracts string value from a SQL chunk if it contains a single string value
579
+ */
580
+ function extractStringValueFromChunk(chunk: SQL): string | undefined {
581
+ if (chunk.queryChunks?.length !== 1 || !chunk.queryChunks[0]) {
582
+ return undefined;
583
+ }
584
+
585
+ const stringChunk = chunk.queryChunks[0];
586
+ if (!("value" in stringChunk)) {
587
+ return undefined;
588
+ }
589
+
590
+ const values = (stringChunk as StringChunk).value;
591
+ if (values?.length === 1) {
592
+ return values[0];
593
+ }
594
+
451
595
  return undefined;
452
596
  }
453
597
 
598
+ function getAliasFromDrizzleAlias(value: unknown): string | undefined {
599
+ if (!isSQLValue(value)) {
600
+ return undefined;
601
+ }
602
+
603
+ const aliasNameChunk = getAliasNameChunk(value.queryChunks);
604
+ if (!aliasNameChunk) {
605
+ return undefined;
606
+ }
607
+
608
+ return extractStringValueFromChunk(aliasNameChunk);
609
+ }
610
+
454
611
  function transformValue(
455
612
  value: unknown,
456
613
  alias: string,
@@ -504,7 +661,7 @@ export function applyFromDriverTransform<T, TSelection>(
504
661
  row as Record<string, unknown>,
505
662
  selections as Record<string, unknown>,
506
663
  aliasMap,
507
- ) as Record<string, unknown>;
664
+ );
508
665
 
509
666
  return processNullBranches(transformed) as unknown as T;
510
667
  });
@@ -555,6 +712,39 @@ export function nextVal(sequenceName: string): number {
555
712
  return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;
556
713
  }
557
714
 
715
+ /**
716
+ * Helper function to build base query for CLUSTER_STATEMENTS_SUMMARY table
717
+ */
718
+ function buildClusterStatementsSummaryQuery(forgeSQLORM: ForgeSqlOperation, timeDiffMs: number) {
719
+ const statementsTable = clusterStatementsSummary;
720
+ return forgeSQLORM
721
+ .getDrizzleQueryBuilder()
722
+ .select({
723
+ digestText: withTidbHint(statementsTable.digestText),
724
+ avgLatency: statementsTable.avgLatency,
725
+ avgMem: statementsTable.avgMem,
726
+ execCount: statementsTable.execCount,
727
+ plan: statementsTable.plan,
728
+ stmtType: statementsTable.stmtType,
729
+ })
730
+ .from(statementsTable)
731
+ .where(
732
+ and(
733
+ isNotNull(statementsTable.digest),
734
+ not(ilike(statementsTable.digestText, "%information_schema%")),
735
+ notInArray(statementsTable.stmtType, ["Use", "Set", "Show", "Commit", "Rollback", "Begin"]),
736
+ gte(
737
+ statementsTable.lastSeen,
738
+ sql`DATE_SUB
739
+ (NOW(), INTERVAL
740
+ ${timeDiffMs * 1000}
741
+ MICROSECOND
742
+ )`,
743
+ ),
744
+ ),
745
+ );
746
+ }
747
+
558
748
  /**
559
749
  * Analyzes and prints query performance data from CLUSTER_STATEMENTS_SUMMARY table.
560
750
  *
@@ -568,7 +758,7 @@ export function nextVal(sequenceName: string): number {
568
758
  *
569
759
  * @param forgeSQLORM - The ForgeSQL operation instance for database access
570
760
  * @param timeDiffMs - Time window in milliseconds to look back for queries (e.g., 1500 for last 1.5 seconds)
571
- * @param timeout - Optional timeout in milliseconds for the query execution (defaults to 1500ms)
761
+ * @param timeout - Optional timeout in milliseconds for the query execution (defaults to 3000ms)
572
762
  *
573
763
  * @example
574
764
  * ```typescript
@@ -587,42 +777,9 @@ export async function printQueriesWithPlan(
587
777
  timeout?: number,
588
778
  ) {
589
779
  try {
590
- const statementsTable = clusterStatementsSummary;
591
780
  const timeoutMs = timeout ?? 3000;
592
781
  const results = await withTimeout(
593
- forgeSQLORM
594
- .getDrizzleQueryBuilder()
595
- .select({
596
- digestText: withTidbHint(statementsTable.digestText),
597
- avgLatency: statementsTable.avgLatency,
598
- avgMem: statementsTable.avgMem,
599
- execCount: statementsTable.execCount,
600
- plan: statementsTable.plan,
601
- stmtType: statementsTable.stmtType,
602
- })
603
- .from(statementsTable)
604
- .where(
605
- and(
606
- isNotNull(statementsTable.digest),
607
- not(ilike(statementsTable.digestText, "%information_schema%")),
608
- notInArray(statementsTable.stmtType, [
609
- "Use",
610
- "Set",
611
- "Show",
612
- "Commit",
613
- "Rollback",
614
- "Begin",
615
- ]),
616
- gte(
617
- statementsTable.lastSeen,
618
- sql`DATE_SUB
619
- (NOW(), INTERVAL
620
- ${timeDiffMs * 1000}
621
- MICROSECOND
622
- )`,
623
- ),
624
- ),
625
- ),
782
+ buildClusterStatementsSummaryQuery(forgeSQLORM, timeDiffMs),
626
783
  `Timeout ${timeoutMs}ms in printQueriesWithPlan - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
627
784
  timeoutMs + 200,
628
785
  );
@@ -647,6 +804,44 @@ export async function printQueriesWithPlan(
647
804
  }
648
805
  }
649
806
 
807
+ export async function handleErrorsWithPlan(
808
+ forgeSQLORM: ForgeSqlOperation,
809
+ timeDiffMs: number,
810
+ type: "OOM" | "TIMEOUT",
811
+ ) {
812
+ try {
813
+ const statementsTable = clusterStatementsSummary;
814
+ const timeoutMs = 3000;
815
+ const baseQuery = buildClusterStatementsSummaryQuery(forgeSQLORM, timeDiffMs);
816
+ const orderColumn = type === "OOM" ? statementsTable.avgMem : statementsTable.avgLatency;
817
+ const query = baseQuery.orderBy(desc(orderColumn)).limit(formatLimitOffset(1));
818
+
819
+ const results = await withTimeout(
820
+ query,
821
+ `Timeout ${timeoutMs}ms in handleErrorsWithPlan - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
822
+ timeoutMs + 200,
823
+ );
824
+
825
+ for (const result of results) {
826
+ // Average execution time (convert from nanoseconds to milliseconds)
827
+ const avgTimeMs = Number(result.avgLatency) / 1_000_000;
828
+ const avgMemMB = Number(result.avgMem) / 1_000_000;
829
+
830
+ // 1. Query info: SQL, memory, time, executions
831
+ // eslint-disable-next-line no-console
832
+ console.warn(
833
+ `SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${result.execCount}\n Plan:${result.plan}`,
834
+ );
835
+ }
836
+ } catch (error) {
837
+ // eslint-disable-next-line no-console
838
+ console.debug(
839
+ `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
840
+ error,
841
+ );
842
+ }
843
+ }
844
+
650
845
  const SESSION_ALIAS_NAME_ORM = "orm";
651
846
 
652
847
  /**
@@ -772,5 +967,5 @@ export function withTidbHint<
772
967
  >(column: AnyMySqlColumn<TPartial>): AnyMySqlColumn<TPartial> {
773
968
  // We lie a bit to TypeScript here: at runtime this is a new SQL fragment,
774
969
  // but returning TExpr keeps the column type info in downstream inference.
775
- return sql`/*+ SET_VAR(tidb_session_alias=${sql.raw(`${SESSION_ALIAS_NAME_ORM}`)}) */ ${column}` as unknown as AnyMySqlColumn<TPartial>;
970
+ return sql`/*+ SET_VAR(tidb_session_alias=${sql.raw(SESSION_ALIAS_NAME_ORM)}) */ ${column}` as unknown as AnyMySqlColumn<TPartial>;
776
971
  }
@@ -50,7 +50,7 @@ export const applySchemaMigrations = async (
50
50
  );
51
51
 
52
52
  migrationHistory = sortedMigrations
53
- .map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toUTCString()}`)
53
+ .map((y) => `${y.id}, ${y.name}, ${y.migratedAt.toISOString()}`)
54
54
  .join("\n");
55
55
  }
56
56
  // eslint-disable-next-line no-console