• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

vzakharchenko / forge-sql-orm / 18982321441

31 Oct 2025 06:52PM UTC coverage: 80.321%. Remained the same
18982321441

push

github

vzakharchenko
new Release

457 of 643 branches covered (71.07%)

Branch coverage included in aggregate %.

26 of 39 new or added lines in 5 files covered. (66.67%)

14 existing lines in 2 files now uncovered.

943 of 1100 relevant lines covered (85.73%)

19.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.5
/src/utils/sqlUtils.ts
1
import {
2
  and,
3
  AnyColumn,
4
  Column,
5
  gte,
6
  ilike,
7
  isNotNull,
8
  isTable,
9
  ne,
10
  not,
11
  notInArray,
12
  SQL,
13
  sql,
14
  StringChunk,
15
} from "drizzle-orm";
16
import { AnyMySqlTable, MySqlCustomColumn } from "drizzle-orm/mysql-core/index";
17
import { DateTime } from "luxon";
18
import { PrimaryKeyBuilder } from "drizzle-orm/mysql-core/primary-keys";
19
import { AnyIndexBuilder } from "drizzle-orm/mysql-core/indexes";
20
import { CheckBuilder } from "drizzle-orm/mysql-core/checks";
21
import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
22
import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
23
import { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
24
import { MySqlTable } from "drizzle-orm/mysql-core";
25
import { isSQLWrapper } from "drizzle-orm/sql/sql";
26
import { clusterStatementsSummary, slowQuery } from "../core/SystemTables";
27
import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
28
import { ColumnDataType } from "drizzle-orm/column-builder";
29
import { AnyMySqlColumn } from "drizzle-orm/mysql-core/columns/common";
30
import type { ColumnBaseConfig } from "drizzle-orm/column";
31

32
/**
33
 * Interface representing table metadata information
34
 */
35
export interface MetadataInfo {
36
  /** The name of the table */
37
  tableName: string;
38
  /** Record of column names and their corresponding column definitions */
39
  columns: Record<string, AnyColumn>;
40
  /** Array of index builders */
41
  indexes: AnyIndexBuilder[];
42
  /** Array of check constraint builders */
43
  checks: CheckBuilder[];
44
  /** Array of foreign key builders */
45
  foreignKeys: ForeignKeyBuilder[];
46
  /** Array of primary key builders */
47
  primaryKeys: PrimaryKeyBuilder[];
48
  /** Array of unique constraint builders */
49
  uniqueConstraints: UniqueConstraintBuilder[];
50
  /** Array of all extra builders */
51
  extras: any[];
52
}
53

54
/**
55
 * Interface for config builder data
56
 */
57
interface ConfigBuilderData {
58
  value?: any;
59
  [key: string]: any;
60
}
61

62
/**
63
 * Parses a date string into a Date object using the specified format
64
 * @param value - The date string to parse or Date
65
 * @param format - The format to use for parsing
66
 * @returns Date object
67
 */
68

69
export const parseDateTime = (value: string | Date, format: string): Date => {
5✔
70
  let result: Date;
71
  if (value instanceof Date) {
17✔
72
    result = value;
2✔
73
  } else {
74
    // 1. Try to parse using the provided format (strict mode)
75
    const dt = DateTime.fromFormat(value, format);
15✔
76
    if (dt.isValid) {
15✔
77
      result = dt.toJSDate();
3✔
78
    } else {
79
      // 2. Try to parse as SQL string
80
      const sqlDt = DateTime.fromSQL(value);
12✔
81
      if (sqlDt.isValid) {
12✔
82
        result = sqlDt.toJSDate();
4✔
83
      } else {
84
        // 3. Try to parse as RFC2822 string
85
        const isoDt = DateTime.fromRFC2822(value);
8✔
86
        if (isoDt.isValid) {
8!
UNCOV
87
          result = isoDt.toJSDate();
×
88
        } else {
89
          // 4. Fallback: use native Date constructor
90
          result = new Date(value);
8✔
91
        }
92
      }
93
    }
94
  }
95
  // 4. Ensure the result is a valid Date object
96
  if (isNaN(result.getTime())) {
17✔
97
    result = new Date(value);
5✔
98
  }
99
  return result;
17✔
100
};
101

102
/**
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.
108
 */
109
export function formatDateTime(
110
  value: Date | string | number,
111
  format: string,
112
  isTimeStamp: boolean,
113
): string {
114
  let dt: DateTime | null = null;
22✔
115

116
  if (value instanceof Date) {
22✔
117
    dt = DateTime.fromJSDate(value);
17✔
118
  } else if (typeof value === "string") {
5✔
119
    for (const parser of [
3✔
120
      DateTime.fromISO,
121
      DateTime.fromRFC2822,
122
      DateTime.fromSQL,
123
      DateTime.fromHTTP,
124
    ]) {
125
      dt = parser(value);
8✔
126
      if (dt.isValid) break;
8✔
127
    }
128
    if (!dt?.isValid) {
3✔
129
      const parsed = Number(value);
1✔
130
      if (!isNaN(parsed)) {
1!
UNCOV
131
        dt = DateTime.fromMillis(parsed);
×
132
      }
133
    }
134
  } else if (typeof value === "number") {
2✔
135
    dt = DateTime.fromMillis(value);
1✔
136
  } else {
137
    throw new Error("Unsupported type");
1✔
138
  }
139

140
  if (!dt?.isValid) {
21✔
141
    throw new Error("Invalid Date");
1✔
142
  }
143
  const minDate = DateTime.fromSeconds(1);
20✔
144
  const maxDate = DateTime.fromMillis(2147483647 * 1000); // 2038-01-19 03:14:07.999 UTC
20✔
145

146
  if (isTimeStamp) {
20✔
147
    if (dt < minDate) {
5✔
148
      throw new Error(
1✔
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) {
4✔
153
      throw new Error(
1✔
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
    }
157
  }
158

159
  return dt.toFormat(format);
18✔
160
}
161

162
/**
163
 * Gets primary keys from the schema.
164
 * @template T - The type of the table schema
165
 * @param {T} table - The table schema
166
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
167
 */
168
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
169
  const { columns, primaryKeys } = getTableMetadata(table);
14✔
170

171
  // First try to find primary keys in columns
172
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
38✔
173
    string,
174
    AnyColumn,
175
  ][];
176

177
  if (columnPrimaryKeys.length > 0) {
14✔
178
    return columnPrimaryKeys;
10✔
179
  }
180

181
  // If no primary keys found in columns, check primary key builders
182
  if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {
4!
183
    // Collect all primary key columns from all primary key builders
184
    const primaryKeyColumns = new Set<[string, AnyColumn]>();
4✔
185

186
    primaryKeys.forEach((primaryKeyBuilder) => {
4✔
187
      // Get primary key columns from each builder
188
      Object.entries(columns)
4✔
189
        .filter(([, column]) => {
190
          // @ts-ignore - PrimaryKeyBuilder has internal columns property
191
          return primaryKeyBuilder.columns.includes(column);
8✔
192
        })
193
        .forEach(([name, column]) => {
194
          primaryKeyColumns.add([name, column]);
4✔
195
        });
196
    });
197

198
    return Array.from(primaryKeyColumns);
4✔
199
  }
200

UNCOV
201
  return [];
×
202
}
203

204
/**
205
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
206
 * @param table - The table schema
207
 * @param foreignKeysSymbol - Symbol for foreign keys
208
 * @param extraSymbol - Symbol for extra configuration
209
 * @returns Array of foreign key builders
210
 */
211
function processForeignKeys(
212
  table: AnyMySqlTable,
213
  foreignKeysSymbol: symbol | undefined,
214
  extraSymbol: symbol | undefined,
215
): ForeignKeyBuilder[] {
216
  const foreignKeys: ForeignKeyBuilder[] = [];
46✔
217

218
  // Process foreign keys from foreignKeysSymbol
219
  if (foreignKeysSymbol) {
46!
220
    // @ts-ignore
221
    const fkArray: any[] = table[foreignKeysSymbol];
46✔
222
    if (fkArray) {
46!
223
      fkArray.forEach((fk) => {
46✔
224
        if (fk.reference) {
×
UNCOV
225
          const item = fk.reference(fk);
×
UNCOV
226
          foreignKeys.push(item);
×
227
        }
228
      });
229
    }
230
  }
231

232
  // Process foreign keys from extraSymbol
233
  if (extraSymbol) {
46!
234
    // @ts-ignore
235
    const extraConfigBuilder = table[extraSymbol];
46✔
236
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
237
      const configBuilderData = extraConfigBuilder(table);
21✔
238
      if (configBuilderData) {
21!
239
        const configBuilders = Array.isArray(configBuilderData)
21!
240
          ? configBuilderData
241
          : Object.values(configBuilderData).map(
UNCOV
242
              (item) => (item as ConfigBuilderData).value ?? item,
×
243
            );
244

245
        configBuilders.forEach((builder) => {
21✔
246
          if (!builder?.constructor) return;
21!
247

248
          const builderName = builder.constructor.name.toLowerCase();
21✔
249
          if (builderName.includes("foreignkeybuilder")) {
21!
UNCOV
250
            foreignKeys.push(builder);
×
251
          }
252
        });
253
      }
254
    }
255
  }
256

257
  return foreignKeys;
46✔
258
}
259

260
/**
261
 * Extracts table metadata from the schema.
262
 * @param {AnyMySqlTable} table - The table schema
263
 * @returns {MetadataInfo} Object containing table metadata
264
 */
265
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
266
  const symbols = Object.getOwnPropertySymbols(table);
46✔
267
  const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
46✔
268
  const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
184✔
269
  const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
460✔
270
  const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
414✔
271

272
  // Initialize builders arrays
273
  const builders = {
46✔
274
    indexes: [] as AnyIndexBuilder[],
275
    checks: [] as CheckBuilder[],
276
    foreignKeys: [] as ForeignKeyBuilder[],
277
    primaryKeys: [] as PrimaryKeyBuilder[],
278
    uniqueConstraints: [] as UniqueConstraintBuilder[],
279
    extras: [] as any[],
280
  };
281

282
  // Process foreign keys
283
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
46✔
284

285
  // Process extra configuration if available
286
  if (extraSymbol) {
46!
287
    // @ts-ignore
288
    const extraConfigBuilder = table[extraSymbol];
46✔
289
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
290
      const configBuilderData = extraConfigBuilder(table);
21✔
291
      if (configBuilderData) {
21!
292
        // Convert configBuilderData to array if it's an object
293
        const configBuilders = Array.isArray(configBuilderData)
21!
294
          ? configBuilderData
295
          : Object.values(configBuilderData).map(
UNCOV
296
              (item) => (item as ConfigBuilderData).value ?? item,
×
297
            );
298

299
        // Process each builder
300
        configBuilders.forEach((builder) => {
21✔
301
          if (!builder?.constructor) return;
21!
302

303
          const builderName = builder.constructor.name.toLowerCase();
21✔
304

305
          // Map builder types to their corresponding arrays
306
          const builderMap = {
21✔
307
            indexbuilder: builders.indexes,
308
            checkbuilder: builders.checks,
309
            primarykeybuilder: builders.primaryKeys,
310
            uniqueconstraintbuilder: builders.uniqueConstraints,
311
          };
312

313
          // Add builder to appropriate array if it matches any type
314
          for (const [type, array] of Object.entries(builderMap)) {
21✔
315
            if (builderName.includes(type)) {
63✔
316
              array.push(builder);
21✔
317
              break;
21✔
318
            }
319
          }
320

321
          // Always add to extras array
322
          builders.extras.push(builder);
21✔
323
        });
324
      }
325
    }
326
  }
327

328
  return {
46✔
329
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
46!
330
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
46!
331
    ...builders,
332
  };
333
}
334

335
/**
336
 * Generates SQL statements for dropping tables and/or their sequences.
337
 *
338
 * @param tables - List of table names to generate DROP statements for.
339
 * @param options - Configuration object:
340
 *   - sequence: whether to drop associated sequences (default: true)
341
 *   - table: whether to drop tables themselves (default: true)
342
 * @returns Array of SQL statements for dropping the specified objects
343
 */
344
export function generateDropTableStatements(
345
  tables: string[],
346
  options?: { sequence: boolean; table: boolean },
347
): string[] {
348
  const dropStatements: string[] = [];
7✔
349
  const validOptions = options ?? { sequence: true, table: true };
7✔
350
  if (!validOptions.sequence && !validOptions.table) {
7✔
351
    // eslint-disable-next-line no-console
352
    console.warn('No drop operations requested: both "table" and "sequence" options are false');
1✔
353
    return [];
1✔
354
  }
355
  tables.forEach((tableName) => {
6✔
356
    if (validOptions.table) {
8✔
357
      dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
7✔
358
    }
359
    if (validOptions.sequence) {
8✔
360
      dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
7✔
361
    }
362
  });
363

364
  return dropStatements;
6✔
365
}
366

367
type AliasColumnMap = Record<string, AnyColumn>;
368

369
function mapSelectTableToAlias(
370
  table: MySqlTable,
371
  uniqPrefix: string,
372
  aliasMap: AliasColumnMap,
373
): any {
374
  const { columns, tableName } = getTableMetadata(table);
10✔
375
  const selectionsTableFields: Record<string, unknown> = {};
10✔
376
  Object.keys(columns).forEach((name) => {
10✔
377
    const column = columns[name] as AnyColumn;
39✔
378
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
39✔
379
    const fieldAlias = sql.raw(uniqName);
39✔
380
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
39✔
381
    aliasMap[uniqName] = column;
39✔
382
  });
383
  return selectionsTableFields;
10✔
384
}
385

386
function isDrizzleColumn(column: any): boolean {
387
  return column && typeof column === "object" && "table" in column;
129✔
388
}
389

390
export function mapSelectAllFieldsToAlias(
391
  selections: any,
392
  name: string,
393
  uniqName: string,
394
  fields: any,
395
  aliasMap: AliasColumnMap,
396
): any {
397
  if (isTable(fields)) {
139✔
398
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
10✔
399
  } else if (isDrizzleColumn(fields)) {
129✔
400
    const column = fields as Column;
107✔
401
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
107✔
402
    let aliasName = sql.raw(uniqAliasName);
107✔
403
    selections[name] = sql`${column} as \`${aliasName}\``;
107✔
404
    aliasMap[uniqAliasName] = column;
107✔
405
  } else if (isSQLWrapper(fields)) {
22✔
406
    selections[name] = fields;
10✔
407
  } else {
408
    const innerSelections: any = {};
12✔
409
    Object.entries(fields).forEach(([iname, ifields]) => {
12✔
410
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
24✔
411
    });
412
    selections[name] = innerSelections;
12✔
413
  }
414
  return selections;
139✔
415
}
416
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
417
  fields: TSelection,
418
): { selections: TSelection; aliasMap: AliasColumnMap } {
419
  if (!fields) {
48!
UNCOV
420
    throw new Error("fields is empty");
×
421
  }
422
  const aliasMap: AliasColumnMap = {};
48✔
423
  const selections: any = {};
48✔
424
  Object.entries(fields).forEach(([name, fields]) => {
48✔
425
    mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);
115✔
426
  });
427
  return { selections, aliasMap };
48✔
428
}
429

430
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
431
  const isSQL =
432
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
224✔
433
  if (isSQL) {
224✔
434
    const sql = value as SQL;
181✔
435
    const queryChunks = sql.queryChunks;
181✔
436
    if (queryChunks.length > 3) {
181✔
437
      const aliasNameChunk = queryChunks[queryChunks.length - 2];
163✔
438
      if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
163!
439
        const aliasNameChunkSql = aliasNameChunk as SQL;
163✔
440
        if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
163!
441
          const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
163✔
442
          if ("value" in queryChunksStringChunc) {
163!
443
            const values = (queryChunksStringChunc as StringChunk).value;
163✔
444
            if (values && values.length === 1) {
163!
445
              return values[0];
163✔
446
            }
447
          }
448
        }
449
      }
450
    }
451
  }
452
  return undefined;
61✔
453
}
454

455
function transformValue(
456
  value: unknown,
457
  alias: string,
458
  aliasMap: Record<string, AnyColumn>,
459
): unknown {
460
  const column = aliasMap[alias];
163✔
461
  if (!column) return value;
163!
462

463
  let customColumn = column as MySqlCustomColumn<any>;
163✔
464
  // @ts-ignore
465
  const fromDriver = customColumn?.mapFrom;
163✔
466
  if (fromDriver && value !== null && value !== undefined) {
163✔
467
    return fromDriver(value);
26✔
468
  }
469
  return value;
137✔
470
}
471

472
function transformObject(
473
  obj: Record<string, unknown>,
474
  selections: Record<string, unknown>,
475
  aliasMap: Record<string, AnyColumn>,
476
): Record<string, unknown> {
477
  const result: Record<string, unknown> = {};
82✔
478

479
  for (const [key, value] of Object.entries(obj)) {
82✔
480
    const selection = selections[key];
224✔
481
    const alias = getAliasFromDrizzleAlias(selection);
224✔
482
    if (alias && aliasMap[alias]) {
224✔
483
      result[key] = transformValue(value, alias, aliasMap);
163✔
484
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
61✔
485
      result[key] = transformObject(
43✔
486
        value as Record<string, unknown>,
487
        selection as Record<string, unknown>,
488
        aliasMap,
489
      );
490
    } else {
491
      result[key] = value;
18✔
492
    }
493
  }
494

495
  return result;
82✔
496
}
497

498
export function applyFromDriverTransform<T, TSelection>(
499
  rows: T[],
500
  selections: TSelection,
501
  aliasMap: Record<string, AnyColumn>,
502
): T[] {
503
  return rows.map((row) => {
31✔
504
    const transformed = transformObject(
39✔
505
      row as Record<string, unknown>,
506
      selections as Record<string, unknown>,
507
      aliasMap,
508
    ) as Record<string, unknown>;
509

510
    return processNullBranches(transformed) as unknown as T;
39✔
511
  });
512
}
513

514
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
515
  if (obj === null || typeof obj !== "object") {
90!
UNCOV
516
    return obj;
×
517
  }
518

519
  // Skip built-in objects like Date, Array, etc.
520
  if (obj.constructor && obj.constructor.name !== "Object") {
90✔
521
    return obj;
8✔
522
  }
523

524
  const result: Record<string, unknown> = {};
82✔
525
  let allNull = true;
82✔
526

527
  for (const [key, value] of Object.entries(obj)) {
82✔
528
    if (value === null || value === undefined) {
224✔
529
      result[key] = null;
5✔
530
      continue;
5✔
531
    }
532

533
    if (typeof value === "object") {
219✔
534
      const processed = processNullBranches(value as Record<string, unknown>);
51✔
535
      result[key] = processed;
51✔
536
      if (processed !== null) {
51✔
537
        allNull = false;
50✔
538
      }
539
    } else {
540
      result[key] = value;
168✔
541
      allNull = false;
168✔
542
    }
543
  }
544

545
  return allNull ? null : result;
82✔
546
}
547

548
export function formatLimitOffset(limitOrOffset: number): number {
549
  if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
5✔
550
    throw new Error("limitOrOffset must be a valid number");
2✔
551
  }
552
  return sql.raw(`${limitOrOffset}`) as unknown as number;
3✔
553
}
554

555
export function nextVal(sequenceName: string): number {
556
  return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;
3✔
557
}
558

559
/**
560
 * Analyzes and prints query performance data from CLUSTER_STATEMENTS_SUMMARY table.
561
 *
562
 * This function queries the CLUSTER_STATEMENTS_SUMMARY table to find queries that were executed
563
 * within the specified time window and prints detailed performance information including:
564
 * - SQL query text
565
 * - Memory usage (average and max in MB)
566
 * - Execution time (average in ms)
567
 * - Number of executions
568
 * - Execution plan
569
 *
570
 * @param forgeSQLORM - The ForgeSQL operation instance for database access
571
 * @param timeDiffMs - Time window in milliseconds to look back for queries (e.g., 1500 for last 1.5 seconds)
572
 * @param timeout - Optional timeout in milliseconds for the query execution (defaults to 1500ms)
573
 *
574
 * @example
575
 * ```typescript
576
 * // Analyze queries from the last 2 seconds
577
 * await printQueriesWithPlan(forgeSQLORM, 2000);
578
 *
579
 * // Analyze queries with custom timeout
580
 * await printQueriesWithPlan(forgeSQLORM, 1000, 3000);
581
 * ```
582
 *
583
 * @throws Does not throw - errors are logged to console.debug instead
584
 */
585
export async function printQueriesWithPlan(
586
  forgeSQLORM: ForgeSqlOperation,
587
  timeDiffMs: number,
588
  timeout?: number,
589
) {
590
  try {
1✔
591
    const statementsTable = clusterStatementsSummary;
1✔
592
    const timeoutMs = timeout ?? 3000;
1✔
593
    const results = await withTimeout(
1✔
594
      forgeSQLORM
595
        .getDrizzleQueryBuilder()
596
        .select({
597
          digestText: withTidbHint(statementsTable.digestText),
598
          avgLatency: statementsTable.avgLatency,
599
          avgMem: statementsTable.avgMem,
600
          execCount: statementsTable.execCount,
601
          plan: statementsTable.plan,
602
          stmtType: statementsTable.stmtType,
603
        })
604
        .from(statementsTable)
605
        .where(
606
          and(
607
            isNotNull(statementsTable.digest),
608
            not(ilike(statementsTable.digestText, "%information_schema%")),
609
            notInArray(statementsTable.stmtType, [
610
              "Use",
611
              "Set",
612
              "Show",
613
              "Commit",
614
              "Rollback",
615
              "Begin",
616
            ]),
617
            gte(
618
              statementsTable.lastSeen,
619
              sql`DATE_SUB
620
                            (NOW(), INTERVAL
621
                            ${timeDiffMs * 1000}
622
                            MICROSECOND
623
                            )`,
624
            ),
625
          ),
626
        ),
627
      `Timeout ${timeoutMs}ms in printQueriesWithPlan - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
628
      timeoutMs + 200,
629
    );
630

UNCOV
631
    results.forEach((result) => {
×
632
      // Average execution time (convert from nanoseconds to milliseconds)
UNCOV
633
      const avgTimeMs = Number(result.avgLatency) / 1_000_000;
×
UNCOV
634
      const avgMemMB = Number(result.avgMem) / 1_000_000;
×
635

636
      // 1. Query info: SQL, memory, time, executions
637
      // eslint-disable-next-line no-console
638
      console.warn(
×
639
        `SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${result.execCount}\n Plan:${result.plan}`,
640
      );
641
    });
642
  } catch (error) {
643
    // eslint-disable-next-line no-console
644
    console.debug(
1✔
645
      `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
1!
646
      error,
647
    );
648
  }
649
}
650

651
const SESSION_ALIAS_NAME_ORM = "orm";
5✔
652

653
/**
654
 * Analyzes and logs slow queries from the last specified number of hours.
655
 *
656
 * This function queries the slow query system table to find queries that were executed
657
 * within the specified time window and logs detailed performance information including:
658
 * - SQL query text
659
 * - Maximum memory usage (in MB)
660
 * - Query execution time (in ms)
661
 * - Execution count
662
 * - Execution plan
663
 *
664
 * @param forgeSQLORM - The ForgeSQL operation instance for database access
665
 * @param hours - Number of hours to look back for slow queries (e.g., 1 for last hour, 24 for last day)
666
 * @param timeout - Optional timeout in milliseconds for the query execution (defaults to 1500ms)
667
 *
668
 * @example
669
 * ```typescript
670
 * // Analyze slow queries from the last hour
671
 * await slowQueryPerHours(forgeSQLORM, 1);
672
 *
673
 * // Analyze slow queries from the last 24 hours with custom timeout
674
 * await slowQueryPerHours(forgeSQLORM, 24, 3000);
675
 *
676
 * // Analyze slow queries from the last 6 hours
677
 * await slowQueryPerHours(forgeSQLORM, 6);
678
 * ```
679
 *
680
 * @throws Does not throw - errors are logged to console.debug instead
681
 */
682
export async function slowQueryPerHours(
683
  forgeSQLORM: ForgeSqlOperation,
684
  hours: number,
685
  timeout?: number,
686
) {
NEW
687
  try {
×
NEW
688
    const timeoutMs = timeout ?? 1500;
×
NEW
689
    const results = await withTimeout(
×
690
      forgeSQLORM
691
        .getDrizzleQueryBuilder()
692
        .select({
693
          query: withTidbHint(slowQuery.query),
694
          queryTime: slowQuery.queryTime,
695
          memMax: slowQuery.memMax,
696
          plan: slowQuery.plan,
697
        })
698
        .from(slowQuery)
699
        .where(
700
          and(
701
            isNotNull(slowQuery.digest),
702
            ne(slowQuery.sessionAlias, SESSION_ALIAS_NAME_ORM),
703
            gte(
704
              slowQuery.time,
705
              sql`DATE_SUB
706
                            (NOW(), INTERVAL
707
                            ${hours}
708
                            HOUR
709
                            )`,
710
            ),
711
          ),
712
        ),
713
      `Timeout ${timeoutMs}ms in slowQueryPerHours - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
714
      timeoutMs,
715
    );
NEW
716
    const response: string[] = [];
×
NEW
717
    results.forEach((result) => {
×
718
      // Convert memory from bytes to MB and handle null values
NEW
719
      const memMaxMB = result.memMax ? Number(result.memMax) / 1_000_000 : 0;
×
720

NEW
721
      const message = `Found SlowQuery SQL: ${result.query} | Memory: ${memMaxMB.toFixed(2)} MB | Time: ${result.queryTime} ms\n Plan:${result.plan}`;
×
NEW
722
      response.push(message);
×
723
      // 1. Query info: SQL, memory, time, executions
724
      // eslint-disable-next-line no-console
NEW
725
      console.warn(message);
×
726
    });
NEW
727
    return response;
×
728
  } catch (error) {
729
    // eslint-disable-next-line no-console
NEW
730
    console.debug(
×
731
      `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
×
732
      error,
733
    );
NEW
734
    return [
×
735
      `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`,
×
736
    ];
737
  }
738
}
739

740
/**
741
 * Executes a promise with a timeout.
742
 *
743
 * @param promise - The promise to execute
744
 * @param timeoutMs - Timeout in milliseconds
745
 * @returns Promise that resolves with the result or rejects on timeout
746
 * @throws {Error} When the operation times out
747
 */
748
export async function withTimeout<T>(
749
  promise: Promise<T>,
750
  message: string,
751
  timeoutMs: number,
752
): Promise<T> {
753
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
754

755
  const timeoutPromise = new Promise<never>((_, reject) => {
97✔
756
    timeoutId = setTimeout(() => {
97✔
757
      reject(new Error(message));
1✔
758
    }, timeoutMs);
759
  });
760

761
  try {
97✔
762
    return await Promise.race([promise, timeoutPromise]);
97✔
763
  } finally {
764
    if (timeoutId) {
97!
765
      clearTimeout(timeoutId);
97✔
766
    }
767
  }
768
}
769

770
export function withTidbHint<
771
  TDataType extends ColumnDataType,
772
  TPartial extends Partial<ColumnBaseConfig<TDataType, string>>,
773
>(column: AnyMySqlColumn<TPartial>): AnyMySqlColumn<TPartial> {
774
  // We lie a bit to TypeScript here: at runtime this is a new SQL fragment,
775
  // but returning TExpr keeps the column type info in downstream inference.
776
  return sql`/*+ SET_VAR(tidb_session_alias=${sql.raw(`${SESSION_ALIAS_NAME_ORM}`)}) */ ${column}` as unknown as AnyMySqlColumn<TPartial>;
3✔
777
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc