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

vzakharchenko / forge-sql-orm / 19836974321

01 Dec 2025 08:46PM UTC coverage: 84.933% (-0.2%) from 85.091%
19836974321

push

github

vzakharchenko
fix issues

659 of 836 branches covered (78.83%)

Branch coverage included in aggregate %.

22 of 25 new or added lines in 3 files covered. (88.0%)

1 existing line in 1 file now uncovered.

1252 of 1414 relevant lines covered (88.54%)

20.41 hits per line

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

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

102
/**
103
 * Parses a string value into DateTime using multiple format parsers
104
 */
105
function parseStringToDateTime(value: string): DateTime | null {
106
  const parsers = [DateTime.fromISO, DateTime.fromRFC2822, DateTime.fromSQL, DateTime.fromHTTP];
3✔
107

108
  for (const parser of parsers) {
3✔
109
    const dt = parser(value);
8✔
110
    if (dt.isValid) {
8✔
111
      return dt;
2✔
112
    }
113
  }
114

115
  // Try parsing as number string
116
  const parsed = Number(value);
1✔
117
  if (!Number.isNaN(parsed)) {
1!
118
    return DateTime.fromMillis(parsed);
×
119
  }
120

121
  return null;
1✔
122
}
123

124
/**
125
 * Converts a value to DateTime
126
 */
127
function valueToDateTime(value: Date | string | number): DateTime {
128
  let dt: DateTime | null = null;
35✔
129

130
  if (value instanceof Date) {
35✔
131
    dt = DateTime.fromJSDate(value);
29✔
132
  } else if (typeof value === "string") {
6✔
133
    dt = parseStringToDateTime(value);
3✔
134
  } else if (typeof value === "number") {
3✔
135
    dt = DateTime.fromMillis(value);
1✔
136
  } else {
137
    throw new Error("Unsupported type");
2✔
138
  }
139

140
  if (!dt?.isValid) {
33✔
141
    throw new Error("Invalid Date");
1✔
142
  }
143

144
  return dt;
32✔
145
}
146

147
/**
148
 * Validates timestamp range for Atlassian Forge compatibility
149
 */
150
function validateTimestampRange(dt: DateTime): void {
151
  const minDate = DateTime.fromSeconds(1);
7✔
152
  const maxDate = DateTime.fromMillis(2147483647 * 1000); // 2038-01-19 03:14:07.999 UTC
7✔
153

154
  if (dt < minDate) {
7✔
155
    throw new Error(
1✔
156
      "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'.",
157
    );
158
  }
159

160
  if (dt > maxDate) {
6✔
161
    throw new Error(
1✔
162
      "Atlassian Forge does not support timestamps beyond 2038-01-19 03:14:07.999999. Please use a smaller date within the supported range.",
163
    );
164
  }
165
}
166

167
/**
168
 * Helper function to validate and format a date-like value using Luxon DateTime.
169
 * @param value - Date object, ISO/RFC2822/SQL/HTTP string, or timestamp (number|string).
170
 * @param format - DateTime format string (Luxon format tokens).
171
 * @param isTimeStamp - Whether to validate timestamp range
172
 * @returns Formatted date string.
173
 * @throws Error if value cannot be parsed as a valid date.
174
 */
175
export function formatDateTime(
176
  value: Date | string | number,
177
  format: string,
178
  isTimeStamp: boolean,
179
): string {
180
  const dt = valueToDateTime(value);
35✔
181

182
  if (isTimeStamp) {
35✔
183
    validateTimestampRange(dt);
7✔
184
  }
185

186
  return dt.toFormat(format);
30✔
187
}
188

189
/**
190
 * Gets primary keys from the schema.
191
 * @template T - The type of the table schema
192
 * @param {T} table - The table schema
193
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
194
 */
195
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
196
  const { columns, primaryKeys } = getTableMetadata(table);
14✔
197

198
  // First try to find primary keys in columns
199
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
38✔
200
    string,
201
    AnyColumn,
202
  ][];
203

204
  if (columnPrimaryKeys.length > 0) {
14✔
205
    return columnPrimaryKeys;
10✔
206
  }
207

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

213
    for (const primaryKeyBuilder of primaryKeys) {
4✔
214
      // Get primary key columns from each builder
215
      for (const [name, column1] of Object.entries(columns).filter(([, column]) => {
4✔
216
        // @ts-ignore - PrimaryKeyBuilder has internal columns property
217
        return primaryKeyBuilder.columns.includes(column);
8✔
218
      })) {
219
        primaryKeyColumns.add([name, column1]);
4✔
220
      }
221
    }
222

223
    return Array.from(primaryKeyColumns);
4✔
224
  }
225

226
  return [];
×
227
}
228

229
/**
230
 * Processes foreign keys from foreignKeysSymbol
231
 */
232
function processForeignKeysFromSymbol(
233
  table: AnyMySqlTable,
234
  foreignKeysSymbol: symbol,
235
): ForeignKeyBuilder[] {
236
  const foreignKeys: ForeignKeyBuilder[] = [];
46✔
237
  // @ts-ignore
238
  const fkArray: any[] = table[foreignKeysSymbol];
46✔
239

240
  if (!fkArray) {
46!
241
    return foreignKeys;
×
242
  }
243

244
  for (const fk of fkArray) {
46✔
245
    if (fk.reference) {
×
246
      const item = fk.reference(fk);
×
247
      foreignKeys.push(item);
×
248
    }
249
  }
250

251
  return foreignKeys;
46✔
252
}
253

254
/**
255
 * Extracts config builders from config builder data
256
 */
257
function extractConfigBuilders(configBuilderData: any): any[] {
258
  if (Array.isArray(configBuilderData)) {
42!
259
    return configBuilderData;
42✔
260
  }
261

262
  return Object.values(configBuilderData).map((item) => (item as ConfigBuilderData).value ?? item);
×
263
}
264

265
/**
266
 * Checks if a builder is a ForeignKeyBuilder
267
 */
268
function isForeignKeyBuilder(builder: any): boolean {
269
  if (!builder?.constructor) {
21!
270
    return false;
×
271
  }
272

273
  const builderName = builder.constructor.name.toLowerCase();
21✔
274
  return builderName.includes("foreignkeybuilder");
21✔
275
}
276

277
/**
278
 * Processes foreign keys from extraSymbol
279
 */
280
function processForeignKeysFromExtra(
281
  table: AnyMySqlTable,
282
  extraSymbol: symbol,
283
): ForeignKeyBuilder[] {
284
  const foreignKeys: ForeignKeyBuilder[] = [];
46✔
285
  // @ts-ignore
286
  const extraConfigBuilder = table[extraSymbol];
46✔
287

288
  if (!extraConfigBuilder || typeof extraConfigBuilder !== "function") {
46✔
289
    return foreignKeys;
25✔
290
  }
291

292
  const configBuilderData = extraConfigBuilder(table);
21✔
293
  if (!configBuilderData) {
21!
294
    return foreignKeys;
×
295
  }
296

297
  const configBuilders = extractConfigBuilders(configBuilderData);
21✔
298

299
  for (const builder of configBuilders) {
21✔
300
    if (isForeignKeyBuilder(builder)) {
21!
301
      foreignKeys.push(builder);
×
302
    }
303
  }
304

305
  return foreignKeys;
21✔
306
}
307

308
/**
309
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
310
 * @param table - The table schema
311
 * @param foreignKeysSymbol - Symbol for foreign keys
312
 * @param extraSymbol - Symbol for extra configuration
313
 * @returns Array of foreign key builders
314
 */
315
function processForeignKeys(
316
  table: AnyMySqlTable,
317
  foreignKeysSymbol: symbol | undefined,
318
  extraSymbol: symbol | undefined,
319
): ForeignKeyBuilder[] {
320
  const foreignKeys: ForeignKeyBuilder[] = [];
46✔
321

322
  // Process foreign keys from foreignKeysSymbol
323
  if (foreignKeysSymbol) {
46!
324
    foreignKeys.push(...processForeignKeysFromSymbol(table, foreignKeysSymbol));
46✔
325
  }
326

327
  // Process foreign keys from extraSymbol
328
  if (extraSymbol) {
46!
329
    foreignKeys.push(...processForeignKeysFromExtra(table, extraSymbol));
46✔
330
  }
331

332
  return foreignKeys;
46✔
333
}
334

335
/**
336
 * Extracts symbols from table schema.
337
 * @param table - The table schema
338
 * @returns Object containing relevant symbols
339
 */
340
function extractTableSymbols(table: AnyMySqlTable) {
341
  const symbols = Object.getOwnPropertySymbols(table);
46✔
342
  return {
46✔
343
    nameSymbol: symbols.find((s) => s.toString().includes("Name")),
46✔
344
    columnsSymbol: symbols.find((s) => s.toString().includes("Columns")),
184✔
345
    foreignKeysSymbol: symbols.find((s) => s.toString().includes("ForeignKeys)")),
460✔
346
    extraSymbol: symbols.find((s) => s.toString().includes("ExtraConfigBuilder")),
414✔
347
  };
348
}
349

350
/**
351
 * Maps builder to appropriate array based on its type.
352
 * @param builder - The builder object
353
 * @param builders - The builders object containing all arrays
354
 * @returns True if builder was added to a specific array, false otherwise
355
 */
356
function addBuilderToTypedArray(
357
  builder: any,
358
  builders: {
359
    indexes: AnyIndexBuilder[];
360
    checks: CheckBuilder[];
361
    primaryKeys: PrimaryKeyBuilder[];
362
    uniqueConstraints: UniqueConstraintBuilder[];
363
  },
364
): boolean {
365
  if (!builder?.constructor) {
21!
366
    return false;
×
367
  }
368

369
  const builderName = builder.constructor.name.toLowerCase();
21✔
370
  const builderMap = {
21✔
371
    indexbuilder: builders.indexes,
372
    checkbuilder: builders.checks,
373
    primarykeybuilder: builders.primaryKeys,
374
    uniqueconstraintbuilder: builders.uniqueConstraints,
375
  };
376

377
  for (const [type, array] of Object.entries(builderMap)) {
21✔
378
    if (builderName.includes(type)) {
63✔
379
      array.push(builder);
21✔
380
      return true;
21✔
381
    }
382
  }
383

384
  return false;
×
385
}
386

387
/**
388
 * Processes extra configuration builders and adds them to the builders object.
389
 * @param table - The table schema
390
 * @param extraSymbol - The extra symbol from table
391
 * @param builders - The builders object to populate
392
 */
393
function processExtraConfigBuilders(
394
  table: AnyMySqlTable,
395
  extraSymbol: symbol | undefined,
396
  builders: {
397
    indexes: AnyIndexBuilder[];
398
    checks: CheckBuilder[];
399
    foreignKeys: ForeignKeyBuilder[];
400
    primaryKeys: PrimaryKeyBuilder[];
401
    uniqueConstraints: UniqueConstraintBuilder[];
402
    extras: any[];
403
  },
404
): void {
405
  if (!extraSymbol) {
46!
406
    return;
×
407
  }
408

409
  // @ts-ignore
410
  const extraConfigBuilder = table[extraSymbol];
46✔
411
  if (!extraConfigBuilder || typeof extraConfigBuilder !== "function") {
46✔
412
    return;
25✔
413
  }
414

415
  const configBuilderData = extraConfigBuilder(table);
21✔
416
  if (!configBuilderData) {
21!
417
    return;
×
418
  }
419

420
  const configBuilders = extractConfigBuilders(configBuilderData);
21✔
421

422
  for (const builder of configBuilders) {
21✔
423
    addBuilderToTypedArray(builder, builders);
21✔
424
    builders.extras.push(builder);
21✔
425
  }
426
}
427

428
/**
429
 * Extracts table metadata from the schema.
430
 * @param {AnyMySqlTable} table - The table schema
431
 * @returns {MetadataInfo} Object containing table metadata
432
 */
433
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
434
  const { nameSymbol, columnsSymbol, foreignKeysSymbol, extraSymbol } = extractTableSymbols(table);
46✔
435

436
  // Initialize builders arrays
437
  const builders = {
46✔
438
    indexes: [] as AnyIndexBuilder[],
439
    checks: [] as CheckBuilder[],
440
    foreignKeys: [] as ForeignKeyBuilder[],
441
    primaryKeys: [] as PrimaryKeyBuilder[],
442
    uniqueConstraints: [] as UniqueConstraintBuilder[],
443
    extras: [] as any[],
444
  };
445

446
  // Process foreign keys
447
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
46✔
448

449
  // Process extra configuration if available
450
  processExtraConfigBuilders(table, extraSymbol, builders);
46✔
451

452
  return {
46✔
453
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
46!
454
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
46!
455
    ...builders,
456
  };
457
}
458

459
/**
460
 * Generates SQL statements for dropping tables and/or their sequences.
461
 *
462
 * @param tables - List of table names to generate DROP statements for.
463
 * @param options - Configuration object:
464
 *   - sequence: whether to drop associated sequences (default: true)
465
 *   - table: whether to drop tables themselves (default: true)
466
 * @returns Array of SQL statements for dropping the specified objects
467
 */
468
export function generateDropTableStatements(
469
  tables: string[],
470
  options?: { sequence: boolean; table: boolean },
471
): string[] {
472
  const dropStatements: string[] = [];
7✔
473
  const validOptions = options ?? { sequence: true, table: true };
7✔
474
  if (!validOptions.sequence && !validOptions.table) {
7✔
475
    // eslint-disable-next-line no-console
476
    console.warn('No drop operations requested: both "table" and "sequence" options are false');
1✔
477
    return [];
1✔
478
  }
479
  for (const tableName of tables) {
6✔
480
    if (validOptions.table) {
8✔
481
      dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
7✔
482
    }
483
    if (validOptions.sequence) {
8✔
484
      dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
7✔
485
    }
486
  }
487

488
  return dropStatements;
6✔
489
}
490

491
type AliasColumnMap = Record<string, AnyColumn>;
492

493
function mapSelectTableToAlias(
494
  table: MySqlTable,
495
  uniqPrefix: string,
496
  aliasMap: AliasColumnMap,
497
): any {
498
  const { columns, tableName } = getTableMetadata(table);
10✔
499
  const selectionsTableFields: Record<string, unknown> = {};
10✔
500
  for (const name of Object.keys(columns)) {
10✔
501
    const column = columns[name] as AnyColumn;
39✔
502
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
39✔
503
    const fieldAlias = sql.raw(uniqName);
39✔
504
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
39✔
505
    aliasMap[uniqName] = column;
39✔
506
  }
507
  return selectionsTableFields;
10✔
508
}
509

510
function isDrizzleColumn(column: any): boolean {
511
  return column && typeof column === "object" && "table" in column;
144✔
512
}
513

514
export function mapSelectAllFieldsToAlias(
515
  selections: any,
516
  name: string,
517
  uniqName: string,
518
  fields: any,
519
  aliasMap: AliasColumnMap,
520
): any {
521
  if (isTable(fields)) {
154✔
522
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
10✔
523
  } else if (isDrizzleColumn(fields)) {
144✔
524
    const column = fields as Column;
122✔
525
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
122✔
526
    let aliasName = sql.raw(uniqAliasName);
122✔
527
    selections[name] = sql`${column} as \`${aliasName}\``;
122✔
528
    aliasMap[uniqAliasName] = column;
122✔
529
  } else if (isSQLWrapper(fields)) {
22✔
530
    selections[name] = fields;
10✔
531
  } else {
532
    const innerSelections: any = {};
12✔
533
    for (const [iname, ifields] of Object.entries(fields)) {
12✔
534
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
24✔
535
    }
536
    selections[name] = innerSelections;
12✔
537
  }
538
  return selections;
154✔
539
}
540
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
541
  fields: TSelection,
542
): { selections: TSelection; aliasMap: AliasColumnMap } {
543
  if (!fields) {
53!
544
    throw new Error("fields is empty");
×
545
  }
546
  const aliasMap: AliasColumnMap = {};
53✔
547
  const selections: any = {};
53✔
548
  for (const [name, fields1] of Object.entries(fields)) {
53✔
549
    mapSelectAllFieldsToAlias(selections, name, name, fields1, aliasMap);
130✔
550
  }
551
  return { selections, aliasMap };
53✔
552
}
553

554
/**
555
 * Checks if value is a SQL object with queryChunks
556
 */
557
function isSQLValue(value: unknown): value is SQL {
558
  return (
230✔
559
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value
877✔
560
  );
561
}
562

563
/**
564
 * Extracts the alias name chunk from query chunks if it exists and is a SQL object
565
 */
566
function getAliasNameChunk(queryChunks: any[]): SQL | undefined {
567
  if (queryChunks.length <= 3) {
187✔
568
    return undefined;
18✔
569
  }
570

571
  const aliasNameChunk = queryChunks[queryChunks.length - 2];
169✔
572
  if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
169!
573
    return aliasNameChunk as SQL;
169✔
574
  }
575

UNCOV
576
  return undefined;
×
577
}
578

579
/**
580
 * Extracts string value from a SQL chunk if it contains a single string value
581
 */
582
function extractStringValueFromChunk(chunk: SQL): string | undefined {
583
  if (chunk.queryChunks?.length !== 1 || !chunk.queryChunks[0]) {
169!
NEW
584
    return undefined;
×
585
  }
586

587
  const stringChunk = chunk.queryChunks[0];
169✔
588
  if (!("value" in stringChunk)) {
169!
NEW
589
    return undefined;
×
590
  }
591

592
  const values = (stringChunk as StringChunk).value;
169✔
593
  if (values?.length === 1) {
169!
594
    return values[0];
169✔
595
  }
596

NEW
597
  return undefined;
×
598
}
599

600
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
601
  if (!isSQLValue(value)) {
230✔
602
    return undefined;
43✔
603
  }
604

605
  const aliasNameChunk = getAliasNameChunk(value.queryChunks);
187✔
606
  if (!aliasNameChunk) {
187✔
607
    return undefined;
18✔
608
  }
609

610
  return extractStringValueFromChunk(aliasNameChunk);
169✔
611
}
612

613
function transformValue(
614
  value: unknown,
615
  alias: string,
616
  aliasMap: Record<string, AnyColumn>,
617
): unknown {
618
  const column = aliasMap[alias];
169✔
619
  if (!column) return value;
169!
620

621
  let customColumn = column as MySqlCustomColumn<any>;
169✔
622
  // @ts-ignore
623
  const fromDriver = customColumn?.mapFrom;
169✔
624
  if (fromDriver && value !== null && value !== undefined) {
169✔
625
    return fromDriver(value);
26✔
626
  }
627
  return value;
143✔
628
}
629

630
function transformObject(
631
  obj: Record<string, unknown>,
632
  selections: Record<string, unknown>,
633
  aliasMap: Record<string, AnyColumn>,
634
): Record<string, unknown> {
635
  const result: Record<string, unknown> = {};
84✔
636

637
  for (const [key, value] of Object.entries(obj)) {
84✔
638
    const selection = selections[key];
230✔
639
    const alias = getAliasFromDrizzleAlias(selection);
230✔
640
    if (alias && aliasMap[alias]) {
230✔
641
      result[key] = transformValue(value, alias, aliasMap);
169✔
642
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
61✔
643
      result[key] = transformObject(
43✔
644
        value as Record<string, unknown>,
645
        selection as Record<string, unknown>,
646
        aliasMap,
647
      );
648
    } else {
649
      result[key] = value;
18✔
650
    }
651
  }
652

653
  return result;
84✔
654
}
655

656
export function applyFromDriverTransform<T, TSelection>(
657
  rows: T[],
658
  selections: TSelection,
659
  aliasMap: Record<string, AnyColumn>,
660
): T[] {
661
  return rows.map((row) => {
33✔
662
    const transformed = transformObject(
41✔
663
      row as Record<string, unknown>,
664
      selections as Record<string, unknown>,
665
      aliasMap,
666
    ) as Record<string, unknown>;
667

668
    return processNullBranches(transformed) as unknown as T;
41✔
669
  });
670
}
671

672
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
673
  if (obj === null || typeof obj !== "object") {
92!
674
    return obj;
×
675
  }
676

677
  // Skip built-in objects like Date, Array, etc.
678
  if (obj.constructor && obj.constructor.name !== "Object") {
92✔
679
    return obj;
8✔
680
  }
681

682
  const result: Record<string, unknown> = {};
84✔
683
  let allNull = true;
84✔
684

685
  for (const [key, value] of Object.entries(obj)) {
84✔
686
    if (value === null || value === undefined) {
230✔
687
      result[key] = null;
5✔
688
      continue;
5✔
689
    }
690

691
    if (typeof value === "object") {
225✔
692
      const processed = processNullBranches(value as Record<string, unknown>);
51✔
693
      result[key] = processed;
51✔
694
      if (processed !== null) {
51✔
695
        allNull = false;
50✔
696
      }
697
    } else {
698
      result[key] = value;
174✔
699
      allNull = false;
174✔
700
    }
701
  }
702

703
  return allNull ? null : result;
84✔
704
}
705

706
export function formatLimitOffset(limitOrOffset: number): number {
707
  if (typeof limitOrOffset !== "number" || Number.isNaN(limitOrOffset)) {
5✔
708
    throw new Error("limitOrOffset must be a valid number");
2✔
709
  }
710
  return sql.raw(`${limitOrOffset}`) as unknown as number;
3✔
711
}
712

713
export function nextVal(sequenceName: string): number {
714
  return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;
3✔
715
}
716

717
/**
718
 * Analyzes and prints query performance data from CLUSTER_STATEMENTS_SUMMARY table.
719
 *
720
 * This function queries the CLUSTER_STATEMENTS_SUMMARY table to find queries that were executed
721
 * within the specified time window and prints detailed performance information including:
722
 * - SQL query text
723
 * - Memory usage (average and max in MB)
724
 * - Execution time (average in ms)
725
 * - Number of executions
726
 * - Execution plan
727
 *
728
 * @param forgeSQLORM - The ForgeSQL operation instance for database access
729
 * @param timeDiffMs - Time window in milliseconds to look back for queries (e.g., 1500 for last 1.5 seconds)
730
 * @param timeout - Optional timeout in milliseconds for the query execution (defaults to 1500ms)
731
 *
732
 * @example
733
 * ```typescript
734
 * // Analyze queries from the last 2 seconds
735
 * await printQueriesWithPlan(forgeSQLORM, 2000);
736
 *
737
 * // Analyze queries with custom timeout
738
 * await printQueriesWithPlan(forgeSQLORM, 1000, 3000);
739
 * ```
740
 *
741
 * @throws Does not throw - errors are logged to console.debug instead
742
 */
743
export async function printQueriesWithPlan(
744
  forgeSQLORM: ForgeSqlOperation,
745
  timeDiffMs: number,
746
  timeout?: number,
747
) {
748
  try {
×
749
    const statementsTable = clusterStatementsSummary;
×
750
    const timeoutMs = timeout ?? 3000;
×
751
    const results = await withTimeout(
×
752
      forgeSQLORM
753
        .getDrizzleQueryBuilder()
754
        .select({
755
          digestText: withTidbHint(statementsTable.digestText),
756
          avgLatency: statementsTable.avgLatency,
757
          avgMem: statementsTable.avgMem,
758
          execCount: statementsTable.execCount,
759
          plan: statementsTable.plan,
760
          stmtType: statementsTable.stmtType,
761
        })
762
        .from(statementsTable)
763
        .where(
764
          and(
765
            isNotNull(statementsTable.digest),
766
            not(ilike(statementsTable.digestText, "%information_schema%")),
767
            notInArray(statementsTable.stmtType, [
768
              "Use",
769
              "Set",
770
              "Show",
771
              "Commit",
772
              "Rollback",
773
              "Begin",
774
            ]),
775
            gte(
776
              statementsTable.lastSeen,
777
              sql`DATE_SUB
778
                            (NOW(), INTERVAL
779
                            ${timeDiffMs * 1000}
780
                            MICROSECOND
781
                            )`,
782
            ),
783
          ),
784
        ),
785
      `Timeout ${timeoutMs}ms in printQueriesWithPlan - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
786
      timeoutMs + 200,
787
    );
788

789
    for (const result of results) {
×
790
      // Average execution time (convert from nanoseconds to milliseconds)
791
      const avgTimeMs = Number(result.avgLatency) / 1_000_000;
×
792
      const avgMemMB = Number(result.avgMem) / 1_000_000;
×
793

794
      // 1. Query info: SQL, memory, time, executions
795
      // eslint-disable-next-line no-console
796
      console.warn(
×
797
        `SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${result.execCount}\n Plan:${result.plan}`,
798
      );
799
    }
800
  } catch (error) {
801
    // eslint-disable-next-line no-console
802
    console.debug(
×
803
      `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
×
804
      error,
805
    );
806
  }
807
}
808

809
const SESSION_ALIAS_NAME_ORM = "orm";
7✔
810

811
/**
812
 * Analyzes and logs slow queries from the last specified number of hours.
813
 *
814
 * This function queries the slow query system table to find queries that were executed
815
 * within the specified time window and logs detailed performance information including:
816
 * - SQL query text
817
 * - Maximum memory usage (in MB)
818
 * - Query execution time (in ms)
819
 * - Execution count
820
 * - Execution plan
821
 *
822
 * @param forgeSQLORM - The ForgeSQL operation instance for database access
823
 * @param hours - Number of hours to look back for slow queries (e.g., 1 for last hour, 24 for last day)
824
 * @param timeout - Optional timeout in milliseconds for the query execution (defaults to 1500ms)
825
 *
826
 * @example
827
 * ```typescript
828
 * // Analyze slow queries from the last hour
829
 * await slowQueryPerHours(forgeSQLORM, 1);
830
 *
831
 * // Analyze slow queries from the last 24 hours with custom timeout
832
 * await slowQueryPerHours(forgeSQLORM, 24, 3000);
833
 *
834
 * // Analyze slow queries from the last 6 hours
835
 * await slowQueryPerHours(forgeSQLORM, 6);
836
 * ```
837
 *
838
 * @throws Does not throw - errors are logged to console.debug instead
839
 */
840
export async function slowQueryPerHours(
841
  forgeSQLORM: ForgeSqlOperation,
842
  hours: number,
843
  timeout?: number,
844
) {
845
  try {
×
846
    const timeoutMs = timeout ?? 1500;
×
847
    const results = await withTimeout(
×
848
      forgeSQLORM
849
        .getDrizzleQueryBuilder()
850
        .select({
851
          query: withTidbHint(slowQuery.query),
852
          queryTime: slowQuery.queryTime,
853
          memMax: slowQuery.memMax,
854
          plan: slowQuery.plan,
855
        })
856
        .from(slowQuery)
857
        .where(
858
          and(
859
            isNotNull(slowQuery.digest),
860
            ne(slowQuery.sessionAlias, SESSION_ALIAS_NAME_ORM),
861
            gte(
862
              slowQuery.time,
863
              sql`DATE_SUB
864
                            (NOW(), INTERVAL
865
                            ${hours}
866
                            HOUR
867
                            )`,
868
            ),
869
          ),
870
        ),
871
      `Timeout ${timeoutMs}ms in slowQueryPerHours - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
872
      timeoutMs,
873
    );
874
    const response: string[] = [];
×
875
    for (const result of results) {
×
876
      // Convert memory from bytes to MB and handle null values
877
      const memMaxMB = result.memMax ? Number(result.memMax) / 1_000_000 : 0;
×
878

879
      const message = `Found SlowQuery SQL: ${result.query} | Memory: ${memMaxMB.toFixed(2)} MB | Time: ${result.queryTime} ms\n Plan:${result.plan}`;
×
880
      response.push(message);
×
881
      // 1. Query info: SQL, memory, time, executions
882
      // eslint-disable-next-line no-console
883
      console.warn(message);
×
884
    }
885
    return response;
×
886
  } catch (error) {
887
    // eslint-disable-next-line no-console
888
    console.debug(
×
889
      `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
×
890
      error,
891
    );
892
    return [
×
893
      `Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`,
×
894
    ];
895
  }
896
}
897

898
/**
899
 * Executes a promise with a timeout.
900
 *
901
 * @param promise - The promise to execute
902
 * @param timeoutMs - Timeout in milliseconds
903
 * @returns Promise that resolves with the result or rejects on timeout
904
 * @throws {Error} When the operation times out
905
 */
906
export async function withTimeout<T>(
907
  promise: Promise<T>,
908
  message: string,
909
  timeoutMs: number,
910
): Promise<T> {
911
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
912

913
  const timeoutPromise = new Promise<never>((_, reject) => {
100✔
914
    timeoutId = setTimeout(() => {
100✔
915
      reject(new Error(message));
1✔
916
    }, timeoutMs);
917
  });
918

919
  try {
100✔
920
    return await Promise.race([promise, timeoutPromise]);
100✔
921
  } finally {
922
    if (timeoutId) {
100!
923
      clearTimeout(timeoutId);
100✔
924
    }
925
  }
926
}
927

928
export function withTidbHint<
929
  TDataType extends ColumnDataType,
930
  TPartial extends Partial<ColumnBaseConfig<TDataType, string>>,
931
>(column: AnyMySqlColumn<TPartial>): AnyMySqlColumn<TPartial> {
932
  // We lie a bit to TypeScript here: at runtime this is a new SQL fragment,
933
  // but returning TExpr keeps the column type info in downstream inference.
934
  return sql`/*+ SET_VAR(tidb_session_alias=${sql.raw(SESSION_ALIAS_NAME_ORM)}) */ ${column}` as unknown as AnyMySqlColumn<TPartial>;
2✔
935
}
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