• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vzakharchenko / forge-sql-orm / 19578591495

21 Nov 2025 05:34PM UTC coverage: 79.762% (-0.4%) from 80.192%
19578591495

push

github

vzakharchenko
fix

457 of 647 branches covered (70.63%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

5 existing lines in 1 file now uncovered.

950 of 1117 relevant lines covered (85.05%)

19.72 hits per line

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

81.55
/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!
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 (Number.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 (!Number.isNaN(parsed)) {
1!
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
    for (const primaryKeyBuilder of primaryKeys) {
4✔
187
      // Get primary key columns from each builder
188
      for (const [name, column1] of Object.entries(columns).filter(([, column]) => {
4✔
189
        // @ts-ignore - PrimaryKeyBuilder has internal columns property
190
        return primaryKeyBuilder.columns.includes(column);
8✔
191
      })) {
192
        primaryKeyColumns.add([name, column1]);
4✔
193
      }
194
    }
195

196
    return Array.from(primaryKeyColumns);
4✔
197
  }
198

199
  return [];
×
200
}
201

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

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

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

243
        for (const builder of configBuilders) {
21✔
244
          if (!builder?.constructor) continue;
21!
245

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

255
  return foreignKeys;
46✔
256
}
257

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

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

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

283
  // Process extra configuration if available
284
  if (extraSymbol) {
46!
285
    // @ts-ignore
286
    const extraConfigBuilder = table[extraSymbol];
46✔
287
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
288
      const configBuilderData = extraConfigBuilder(table);
21✔
289
      if (configBuilderData) {
21!
290
        // Convert configBuilderData to array if it's an object
291
        const configBuilders = Array.isArray(configBuilderData)
21!
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) {
21✔
299
          if (!builder?.constructor) continue;
21!
300

301
          const builderName = builder.constructor.name.toLowerCase();
21✔
302

303
          // Map builder types to their corresponding arrays
304
          const builderMap = {
21✔
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)) {
21✔
313
            if (builderName.includes(type)) {
63✔
314
              array.push(builder);
21✔
315
              break;
21✔
316
            }
317
          }
318

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

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

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

362
  return dropStatements;
6✔
363
}
364

365
type AliasColumnMap = Record<string, AnyColumn>;
366

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

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

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

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

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

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

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

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

494
  return result;
82✔
495
}
496

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

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

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

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

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

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

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

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

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

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

558
/**
559
 * Analyzes and prints query performance data from CLUSTER_STATEMENTS_SUMMARY table.
560
 *
561
 * This function queries the CLUSTER_STATEMENTS_SUMMARY table to find queries that were executed
562
 * within the specified time window and prints detailed performance information including:
563
 * - SQL query text
564
 * - Memory usage (average and max in MB)
565
 * - Execution time (average in ms)
566
 * - Number of executions
567
 * - Execution plan
568
 *
569
 * @param forgeSQLORM - The ForgeSQL operation instance for database access
570
 * @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)
572
 *
573
 * @example
574
 * ```typescript
575
 * // Analyze queries from the last 2 seconds
576
 * await printQueriesWithPlan(forgeSQLORM, 2000);
577
 *
578
 * // Analyze queries with custom timeout
579
 * await printQueriesWithPlan(forgeSQLORM, 1000, 3000);
580
 * ```
581
 *
582
 * @throws Does not throw - errors are logged to console.debug instead
583
 */
584
export async function printQueriesWithPlan(
585
  forgeSQLORM: ForgeSqlOperation,
586
  timeDiffMs: number,
587
  timeout?: number,
588
) {
UNCOV
589
  try {
×
UNCOV
590
    const statementsTable = clusterStatementsSummary;
×
UNCOV
591
    const timeoutMs = timeout ?? 3000;
×
UNCOV
592
    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
        ),
626
      `Timeout ${timeoutMs}ms in printQueriesWithPlan - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
627
      timeoutMs + 200,
628
    );
629

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

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

650
const SESSION_ALIAS_NAME_ORM = "orm";
5✔
651

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

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

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

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

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

769
export function withTidbHint<
770
  TDataType extends ColumnDataType,
771
  TPartial extends Partial<ColumnBaseConfig<TDataType, string>>,
772
>(column: AnyMySqlColumn<TPartial>): AnyMySqlColumn<TPartial> {
773
  // We lie a bit to TypeScript here: at runtime this is a new SQL fragment,
774
  // 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>;
2✔
776
}
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