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

vzakharchenko / forge-sql-orm / 17729828783

15 Sep 2025 10:21AM UTC coverage: 87.193% (-0.8%) from 87.953%
17729828783

push

github

vzakharchenko
new release

462 of 555 branches covered (83.24%)

Branch coverage included in aggregate %.

45 of 70 new or added lines in 3 files covered. (64.29%)

1989 of 2256 relevant lines covered (88.16%)

15.06 hits per line

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

86.81
/src/utils/sqlUtils.ts
1
import { AnyColumn, Column, isTable, SQL, sql, StringChunk } from "drizzle-orm";
1✔
2
import { AnyMySqlTable, MySqlCustomColumn } from "drizzle-orm/mysql-core/index";
3
import { DateTime } from "luxon";
1✔
4
import { PrimaryKeyBuilder } from "drizzle-orm/mysql-core/primary-keys";
5
import { AnyIndexBuilder } from "drizzle-orm/mysql-core/indexes";
6
import { CheckBuilder } from "drizzle-orm/mysql-core/checks";
7
import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
8
import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
9
import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
10
import { MySqlTable } from "drizzle-orm/mysql-core";
11
import { isSQLWrapper } from "drizzle-orm/sql/sql";
1✔
12

13
/**
14
 * Interface representing table metadata information
15
 */
16
export interface MetadataInfo {
17
  /** The name of the table */
18
  tableName: string;
19
  /** Record of column names and their corresponding column definitions */
20
  columns: Record<string, AnyColumn>;
21
  /** Array of index builders */
22
  indexes: AnyIndexBuilder[];
23
  /** Array of check constraint builders */
24
  checks: CheckBuilder[];
25
  /** Array of foreign key builders */
26
  foreignKeys: ForeignKeyBuilder[];
27
  /** Array of primary key builders */
28
  primaryKeys: PrimaryKeyBuilder[];
29
  /** Array of unique constraint builders */
30
  uniqueConstraints: UniqueConstraintBuilder[];
31
  /** Array of all extra builders */
32
  extras: any[];
33
}
34

35
/**
36
 * Interface for config builder data
37
 */
38
interface ConfigBuilderData {
39
  value?: any;
40
  [key: string]: any;
41
}
42

43
/**
44
 * Parses a date string into a Date object using the specified format
45
 * @param value - The date string to parse or Date
46
 * @param format - The format to use for parsing
47
 * @returns Date object
48
 */
49

50
export const parseDateTime = (value: string | Date, format: string): Date => {
1✔
51
  let result: Date;
13✔
52
  if (value instanceof Date) {
13✔
53
    result = value;
1✔
54
  } else {
13✔
55
    // 1. Try to parse using the provided format (strict mode)
56
    const dt = DateTime.fromFormat(value, format);
12✔
57
    if (dt.isValid) {
12✔
58
      result = dt.toJSDate();
4✔
59
    } else {
8✔
60
      // 2. Try to parse as ISO string
61
      const isoDt = DateTime.fromISO(value);
8✔
62
      if (isoDt.isValid) {
8!
63
        result = isoDt.toJSDate();
×
64
      } else {
8✔
65
        // 3. Fallback: use native Date constructor
66
        result = new Date(value);
8✔
67
      }
8✔
68
    }
8✔
69
  }
12✔
70
  // 4. Ensure the result is a valid Date object
71
  if (isNaN(result.getTime())) {
13✔
72
    result = new Date(value);
4✔
73
  }
4✔
74
  return result;
13✔
75
};
13✔
76

77
/**
78
 * Helper function to validate and format a date-like value using Luxon DateTime.
79
 * @param value - Date object, ISO/RFC2822/SQL/HTTP string, or timestamp (number|string).
80
 * @param format - DateTime format string (Luxon format tokens).
81
 * @returns Formatted date string.
82
 * @throws Error if value cannot be parsed as a valid date.
83
 */
84
export function formatDateTime(value: Date | string | number, format: string): string {
1✔
85
  let dt: DateTime | null = null;
13✔
86

87
  if (value instanceof Date) {
13✔
88
    dt = DateTime.fromJSDate(value);
13✔
89
  } else if (typeof value === "string") {
13!
NEW
90
    for (const parser of [
×
NEW
91
      DateTime.fromISO,
×
NEW
92
      DateTime.fromRFC2822,
×
NEW
93
      DateTime.fromSQL,
×
NEW
94
      DateTime.fromHTTP,
×
NEW
95
    ]) {
×
NEW
96
      dt = parser(value);
×
NEW
97
      if (dt.isValid) break;
×
NEW
98
    }
×
NEW
99
    if (!dt?.isValid) {
×
NEW
100
      const parsed = Number(value);
×
NEW
101
      if (!isNaN(parsed)) {
×
NEW
102
        dt = DateTime.fromMillis(parsed);
×
NEW
103
      }
×
NEW
104
    }
×
NEW
105
  } else if (typeof value === "number") {
×
NEW
106
    dt = DateTime.fromMillis(value);
×
107
  } else {
×
NEW
108
    throw new Error("Unsupported type");
×
NEW
109
  }
×
110

111
  if (!dt?.isValid) {
13!
112
    throw new Error("Invalid Date");
×
113
  }
×
114

115
  return dt.toFormat(format);
13✔
116
}
13✔
117

118
/**
119
 * Gets primary keys from the schema.
120
 * @template T - The type of the table schema
121
 * @param {T} table - The table schema
122
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
123
 */
124
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
1✔
125
  const { columns, primaryKeys } = getTableMetadata(table);
14✔
126

127
  // First try to find primary keys in columns
128
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
14✔
129
    string,
130
    AnyColumn,
131
  ][];
132

133
  if (columnPrimaryKeys.length > 0) {
14✔
134
    return columnPrimaryKeys;
10✔
135
  }
10✔
136

137
  // If no primary keys found in columns, check primary key builders
138
  if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {
14✔
139
    // Collect all primary key columns from all primary key builders
140
    const primaryKeyColumns = new Set<[string, AnyColumn]>();
4✔
141

142
    primaryKeys.forEach((primaryKeyBuilder) => {
4✔
143
      // Get primary key columns from each builder
144
      Object.entries(columns)
4✔
145
        .filter(([, column]) => {
4✔
146
          // @ts-ignore - PrimaryKeyBuilder has internal columns property
147
          return primaryKeyBuilder.columns.includes(column);
8✔
148
        })
4✔
149
        .forEach(([name, column]) => {
4✔
150
          primaryKeyColumns.add([name, column]);
4✔
151
        });
4✔
152
    });
4✔
153

154
    return Array.from(primaryKeyColumns);
4✔
155
  }
4!
156

157
  return [];
×
158
}
×
159

160
/**
161
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
162
 * @param table - The table schema
163
 * @param foreignKeysSymbol - Symbol for foreign keys
164
 * @param extraSymbol - Symbol for extra configuration
165
 * @returns Array of foreign key builders
166
 */
167
function processForeignKeys(
46✔
168
  table: AnyMySqlTable,
46✔
169
  foreignKeysSymbol: symbol | undefined,
46✔
170
  extraSymbol: symbol | undefined,
46✔
171
): ForeignKeyBuilder[] {
46✔
172
  const foreignKeys: ForeignKeyBuilder[] = [];
46✔
173

174
  // Process foreign keys from foreignKeysSymbol
175
  if (foreignKeysSymbol) {
46✔
176
    // @ts-ignore
177
    const fkArray: any[] = table[foreignKeysSymbol];
46✔
178
    if (fkArray) {
46✔
179
      fkArray.forEach((fk) => {
46✔
180
        if (fk.reference) {
×
181
          const item = fk.reference(fk);
×
182
          foreignKeys.push(item);
×
183
        }
×
184
      });
46✔
185
    }
46✔
186
  }
46✔
187

188
  // Process foreign keys from extraSymbol
189
  if (extraSymbol) {
46✔
190
    // @ts-ignore
191
    const extraConfigBuilder = table[extraSymbol];
46✔
192
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
193
      const configBuilderData = extraConfigBuilder(table);
21✔
194
      if (configBuilderData) {
21✔
195
        const configBuilders = Array.isArray(configBuilderData)
21✔
196
          ? configBuilderData
21!
197
          : Object.values(configBuilderData).map(
×
198
              (item) => (item as ConfigBuilderData).value ?? item,
×
199
            );
×
200

201
        configBuilders.forEach((builder) => {
21✔
202
          if (!builder?.constructor) return;
21!
203

204
          const builderName = builder.constructor.name.toLowerCase();
21✔
205
          if (builderName.includes("foreignkeybuilder")) {
21!
206
            foreignKeys.push(builder);
×
207
          }
×
208
        });
21✔
209
      }
21✔
210
    }
21✔
211
  }
46✔
212

213
  return foreignKeys;
46✔
214
}
46✔
215

216
/**
217
 * Extracts table metadata from the schema.
218
 * @param {AnyMySqlTable} table - The table schema
219
 * @returns {MetadataInfo} Object containing table metadata
220
 */
221
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
1✔
222
  const symbols = Object.getOwnPropertySymbols(table);
46✔
223
  const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
46✔
224
  const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
46✔
225
  const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
46✔
226
  const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
46✔
227

228
  // Initialize builders arrays
229
  const builders = {
46✔
230
    indexes: [] as AnyIndexBuilder[],
46✔
231
    checks: [] as CheckBuilder[],
46✔
232
    foreignKeys: [] as ForeignKeyBuilder[],
46✔
233
    primaryKeys: [] as PrimaryKeyBuilder[],
46✔
234
    uniqueConstraints: [] as UniqueConstraintBuilder[],
46✔
235
    extras: [] as any[],
46✔
236
  };
46✔
237

238
  // Process foreign keys
239
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
46✔
240

241
  // Process extra configuration if available
242
  if (extraSymbol) {
46✔
243
    // @ts-ignore
244
    const extraConfigBuilder = table[extraSymbol];
46✔
245
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
246
      const configBuilderData = extraConfigBuilder(table);
21✔
247
      if (configBuilderData) {
21✔
248
        // Convert configBuilderData to array if it's an object
249
        const configBuilders = Array.isArray(configBuilderData)
21✔
250
          ? configBuilderData
21!
251
          : Object.values(configBuilderData).map(
×
252
              (item) => (item as ConfigBuilderData).value ?? item,
×
253
            );
×
254

255
        // Process each builder
256
        configBuilders.forEach((builder) => {
21✔
257
          if (!builder?.constructor) return;
21!
258

259
          const builderName = builder.constructor.name.toLowerCase();
21✔
260

261
          // Map builder types to their corresponding arrays
262
          const builderMap = {
21✔
263
            indexbuilder: builders.indexes,
21✔
264
            checkbuilder: builders.checks,
21✔
265
            primarykeybuilder: builders.primaryKeys,
21✔
266
            uniqueconstraintbuilder: builders.uniqueConstraints,
21✔
267
          };
21✔
268

269
          // Add builder to appropriate array if it matches any type
270
          for (const [type, array] of Object.entries(builderMap)) {
21✔
271
            if (builderName.includes(type)) {
63✔
272
              array.push(builder);
21✔
273
              break;
21✔
274
            }
21✔
275
          }
63✔
276

277
          // Always add to extras array
278
          builders.extras.push(builder);
21✔
279
        });
21✔
280
      }
21✔
281
    }
21✔
282
  }
46✔
283

284
  return {
46✔
285
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
46!
286
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
46!
287
    ...builders,
46✔
288
  };
46✔
289
}
46✔
290

291
/**
292
 * Generates SQL statements for dropping tables and/or their sequences.
293
 *
294
 * @param tables - List of table names to generate DROP statements for.
295
 * @param options - Configuration object:
296
 *   - sequence: whether to drop associated sequences (default: true)
297
 *   - table: whether to drop tables themselves (default: true)
298
 * @returns Array of SQL statements for dropping the specified objects
299
 */
300
export function generateDropTableStatements(
1✔
301
  tables: string[],
4✔
302
  options?: { sequence: boolean; table: boolean },
4✔
303
): string[] {
4✔
304
  const dropStatements: string[] = [];
4✔
305
  const validOptions = options ?? { sequence: true, table: true };
4✔
306
  if (!validOptions.sequence && !validOptions.table) {
4!
307
    console.warn('No drop operations requested: both "table" and "sequence" options are false');
×
308
    return [];
×
309
  }
×
310
  tables.forEach((tableName) => {
4✔
311
    if (validOptions.table) {
6✔
312
      dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
6✔
313
    }
6✔
314
    if (validOptions.sequence) {
6✔
315
      dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
6✔
316
    }
6✔
317
  });
4✔
318

319
  return dropStatements;
4✔
320
}
4✔
321

322
type AliasColumnMap = Record<string, AnyColumn>;
323

324
function mapSelectTableToAlias(
10✔
325
  table: MySqlTable,
10✔
326
  uniqPrefix: string,
10✔
327
  aliasMap: AliasColumnMap,
10✔
328
): any {
10✔
329
  const { columns, tableName } = getTableMetadata(table);
10✔
330
  const selectionsTableFields: Record<string, unknown> = {};
10✔
331
  Object.keys(columns).forEach((name) => {
10✔
332
    const column = columns[name] as AnyColumn;
39✔
333
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
39✔
334
    const fieldAlias = sql.raw(uniqName);
39✔
335
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
39✔
336
    aliasMap[uniqName] = column;
39✔
337
  });
10✔
338
  return selectionsTableFields;
10✔
339
}
10✔
340

341
function isDrizzleColumn(column: any): boolean {
79✔
342
  return column && typeof column === "object" && "table" in column;
79✔
343
}
79✔
344

345
export function mapSelectAllFieldsToAlias(
1✔
346
  selections: any,
89✔
347
  name: string,
89✔
348
  uniqName: string,
89✔
349
  fields: any,
89✔
350
  aliasMap: AliasColumnMap,
89✔
351
): any {
89✔
352
  if (isTable(fields)) {
89✔
353
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
10✔
354
  } else if (isDrizzleColumn(fields)) {
89✔
355
    const column = fields as Column;
58✔
356
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
58✔
357
    let aliasName = sql.raw(uniqAliasName);
58✔
358
    selections[name] = sql`${column} as \`${aliasName}\``;
58✔
359
    aliasMap[uniqAliasName] = column;
58✔
360
  } else if (isSQLWrapper(fields)) {
79✔
361
    selections[name] = fields;
10✔
362
  } else {
21✔
363
    const innerSelections: any = {};
11✔
364
    Object.entries(fields).forEach(([iname, ifields]) => {
11✔
365
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
22✔
366
    });
11✔
367
    selections[name] = innerSelections;
11✔
368
  }
11✔
369
  return selections;
89✔
370
}
89✔
371
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
1✔
372
  fields: TSelection,
29✔
373
): { selections: TSelection; aliasMap: AliasColumnMap } {
29✔
374
  if (!fields) {
29!
375
    throw new Error("fields is empty");
×
376
  }
×
377
  const aliasMap: AliasColumnMap = {};
29✔
378
  const selections: any = {};
29✔
379
  Object.entries(fields).forEach(([name, fields]) => {
29✔
380
    mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);
67✔
381
  });
29✔
382
  return { selections, aliasMap };
29✔
383
}
29✔
384

385
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
200✔
386
  const isSQL =
200✔
387
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
200✔
388
  if (isSQL) {
200✔
389
    const sql = value as SQL;
158✔
390
    const queryChunks = sql.queryChunks;
158✔
391
    if (queryChunks.length > 3) {
158✔
392
      const aliasNameChunk = queryChunks[queryChunks.length - 2];
140✔
393
      if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
140✔
394
        const aliasNameChunkSql = aliasNameChunk as SQL;
140✔
395
        if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
140✔
396
          const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
140✔
397
          if ("value" in queryChunksStringChunc) {
140✔
398
            const values = (queryChunksStringChunc as StringChunk).value;
140✔
399
            if (values && values.length === 1) {
140✔
400
              return values[0];
140✔
401
            }
140✔
402
          }
140✔
403
        }
140✔
404
      }
140✔
405
    }
140✔
406
  }
158✔
407
  return undefined;
60✔
408
}
60✔
409

410
function transformValue(
140✔
411
  value: unknown,
140✔
412
  alias: string,
140✔
413
  aliasMap: Record<string, AnyColumn>,
140✔
414
): unknown {
140✔
415
  const column = aliasMap[alias];
140✔
416
  if (!column) return value;
140!
417

418
  let customColumn = column as MySqlCustomColumn<any>;
140✔
419
  // @ts-ignore
420
  const fromDriver = customColumn?.mapFrom;
140✔
421
  if (fromDriver && value !== null && value !== undefined) {
140✔
422
    return fromDriver(value);
26✔
423
  }
26✔
424
  return value;
114✔
425
}
114✔
426

427
function transformObject(
71✔
428
  obj: Record<string, unknown>,
71✔
429
  selections: Record<string, unknown>,
71✔
430
  aliasMap: Record<string, AnyColumn>,
71✔
431
): Record<string, unknown> {
71✔
432
  const result: Record<string, unknown> = {};
71✔
433

434
  for (const [key, value] of Object.entries(obj)) {
71✔
435
    const selection = selections[key];
200✔
436
    const alias = getAliasFromDrizzleAlias(selection);
200✔
437
    if (alias && aliasMap[alias]) {
200✔
438
      result[key] = transformValue(value, alias, aliasMap);
140✔
439
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
200✔
440
      result[key] = transformObject(
42✔
441
        value as Record<string, unknown>,
42✔
442
        selection as Record<string, unknown>,
42✔
443
        aliasMap,
42✔
444
      );
42✔
445
    } else {
60✔
446
      result[key] = value;
18✔
447
    }
18✔
448
  }
200✔
449

450
  return result;
71✔
451
}
71✔
452

453
export function applyFromDriverTransform<T, TSelection>(
1✔
454
  rows: T[],
21✔
455
  selections: TSelection,
21✔
456
  aliasMap: Record<string, AnyColumn>,
21✔
457
): T[] {
21✔
458
  return rows.map((row) => {
21✔
459
    const transformed = transformObject(
29✔
460
      row as Record<string, unknown>,
29✔
461
      selections as Record<string, unknown>,
29✔
462
      aliasMap,
29✔
463
    ) as Record<string, unknown>;
29✔
464

465
    return processNullBranches(transformed) as unknown as T;
29✔
466
  });
21✔
467
}
21✔
468

469
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
79✔
470
  if (obj === null || typeof obj !== "object") {
79!
471
    return obj;
×
472
  }
×
473

474
  // Skip built-in objects like Date, Array, etc.
475
  if (obj.constructor && obj.constructor.name !== "Object") {
79✔
476
    return obj;
8✔
477
  }
8✔
478

479
  const result: Record<string, unknown> = {};
71✔
480
  let allNull = true;
71✔
481

482
  for (const [key, value] of Object.entries(obj)) {
79✔
483
    if (value === null || value === undefined) {
200✔
484
      result[key] = null;
5✔
485
      continue;
5✔
486
    }
5✔
487

488
    if (typeof value === "object") {
200✔
489
      const processed = processNullBranches(value as Record<string, unknown>);
50✔
490
      result[key] = processed;
50✔
491
      if (processed !== null) {
50✔
492
        allNull = false;
49✔
493
      }
49✔
494
    } else {
196✔
495
      result[key] = value;
145✔
496
      allNull = false;
145✔
497
    }
145✔
498
  }
200✔
499

500
  return allNull ? null : result;
79✔
501
}
79✔
502

503
export function formatLimitOffset(limitOrOffset: number): number {
1✔
504
  if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
5✔
505
    throw new Error("limitOrOffset must be a valid number");
2✔
506
  }
2✔
507
  return sql.raw(`${limitOrOffset}`) as unknown as number;
3✔
508
}
3✔
509

510
export function nextVal(sequenceName: string): number {
1✔
511
  return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;
3✔
512
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc