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

vzakharchenko / forge-sql-orm / 17695420824

13 Sep 2025 10:37AM UTC coverage: 80.749% (-0.05%) from 80.8%
17695420824

push

github

vzakharchenko
fixed date issues

364 of 445 branches covered (81.8%)

Branch coverage included in aggregate %.

9 of 13 new or added lines in 2 files covered. (69.23%)

1641 of 2038 relevant lines covered (80.52%)

11.19 hits per line

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

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

77
/**
78
 * Helper function to validate and format Date objects using DateTime
79
 * @param value - Date object to validate and format
80
 * @param format - DateTime format string
81
 * @returns Formatted date string
82
 * @throws Error if date is invalid
83
 */
84
export function formatDateTime(value: Date, format: string): string {
1✔
85
  const fromJSDate = DateTime.fromJSDate(value);
10✔
86
  if (fromJSDate.isValid) {
10✔
87
    return fromJSDate.toFormat(format);
10✔
88
  } else {
10!
NEW
89
    throw new Error("Invalid Date");
×
NEW
90
  }
×
91
}
10✔
92

93
/**
94
 * Gets primary keys from the schema.
95
 * @template T - The type of the table schema
96
 * @param {T} table - The table schema
97
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
98
 */
99
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
1✔
100
  const { columns, primaryKeys } = getTableMetadata(table);
14✔
101

102
  // First try to find primary keys in columns
103
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
14✔
104
    string,
105
    AnyColumn,
106
  ][];
107

108
  if (columnPrimaryKeys.length > 0) {
14✔
109
    return columnPrimaryKeys;
10✔
110
  }
10✔
111

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

117
    primaryKeys.forEach((primaryKeyBuilder) => {
4✔
118
      // Get primary key columns from each builder
119
      Object.entries(columns)
4✔
120
        .filter(([, column]) => {
4✔
121
          // @ts-ignore - PrimaryKeyBuilder has internal columns property
122
          return primaryKeyBuilder.columns.includes(column);
8✔
123
        })
4✔
124
        .forEach(([name, column]) => {
4✔
125
          primaryKeyColumns.add([name, column]);
4✔
126
        });
4✔
127
    });
4✔
128

129
    return Array.from(primaryKeyColumns);
4✔
130
  }
4!
131

132
  return [];
×
133
}
×
134

135
/**
136
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
137
 * @param table - The table schema
138
 * @param foreignKeysSymbol - Symbol for foreign keys
139
 * @param extraSymbol - Symbol for extra configuration
140
 * @returns Array of foreign key builders
141
 */
142
function processForeignKeys(
44✔
143
  table: AnyMySqlTable,
44✔
144
  foreignKeysSymbol: symbol | undefined,
44✔
145
  extraSymbol: symbol | undefined,
44✔
146
): ForeignKeyBuilder[] {
44✔
147
  const foreignKeys: ForeignKeyBuilder[] = [];
44✔
148

149
  // Process foreign keys from foreignKeysSymbol
150
  if (foreignKeysSymbol) {
44✔
151
    // @ts-ignore
152
    const fkArray: any[] = table[foreignKeysSymbol];
44✔
153
    if (fkArray) {
44✔
154
      fkArray.forEach((fk) => {
44✔
155
        if (fk.reference) {
×
156
          const item = fk.reference(fk);
×
157
          foreignKeys.push(item);
×
158
        }
×
159
      });
44✔
160
    }
44✔
161
  }
44✔
162

163
  // Process foreign keys from extraSymbol
164
  if (extraSymbol) {
44✔
165
    // @ts-ignore
166
    const extraConfigBuilder = table[extraSymbol];
44✔
167
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
44✔
168
      const configBuilderData = extraConfigBuilder(table);
19✔
169
      if (configBuilderData) {
19✔
170
        const configBuilders = Array.isArray(configBuilderData)
19✔
171
          ? configBuilderData
19!
172
          : Object.values(configBuilderData).map(
×
173
              (item) => (item as ConfigBuilderData).value ?? item,
×
174
            );
×
175

176
        configBuilders.forEach((builder) => {
19✔
177
          if (!builder?.constructor) return;
19!
178

179
          const builderName = builder.constructor.name.toLowerCase();
19✔
180
          if (builderName.includes("foreignkeybuilder")) {
19!
181
            foreignKeys.push(builder);
×
182
          }
×
183
        });
19✔
184
      }
19✔
185
    }
19✔
186
  }
44✔
187

188
  return foreignKeys;
44✔
189
}
44✔
190

191
/**
192
 * Extracts table metadata from the schema.
193
 * @param {AnyMySqlTable} table - The table schema
194
 * @returns {MetadataInfo} Object containing table metadata
195
 */
196
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
1✔
197
  const symbols = Object.getOwnPropertySymbols(table);
44✔
198
  const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
44✔
199
  const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
44✔
200
  const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
44✔
201
  const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
44✔
202

203
  // Initialize builders arrays
204
  const builders = {
44✔
205
    indexes: [] as AnyIndexBuilder[],
44✔
206
    checks: [] as CheckBuilder[],
44✔
207
    foreignKeys: [] as ForeignKeyBuilder[],
44✔
208
    primaryKeys: [] as PrimaryKeyBuilder[],
44✔
209
    uniqueConstraints: [] as UniqueConstraintBuilder[],
44✔
210
    extras: [] as any[],
44✔
211
  };
44✔
212

213
  // Process foreign keys
214
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
44✔
215

216
  // Process extra configuration if available
217
  if (extraSymbol) {
44✔
218
    // @ts-ignore
219
    const extraConfigBuilder = table[extraSymbol];
44✔
220
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
44✔
221
      const configBuilderData = extraConfigBuilder(table);
19✔
222
      if (configBuilderData) {
19✔
223
        // Convert configBuilderData to array if it's an object
224
        const configBuilders = Array.isArray(configBuilderData)
19✔
225
          ? configBuilderData
19!
226
          : Object.values(configBuilderData).map(
×
227
              (item) => (item as ConfigBuilderData).value ?? item,
×
228
            );
×
229

230
        // Process each builder
231
        configBuilders.forEach((builder) => {
19✔
232
          if (!builder?.constructor) return;
19!
233

234
          const builderName = builder.constructor.name.toLowerCase();
19✔
235

236
          // Map builder types to their corresponding arrays
237
          const builderMap = {
19✔
238
            indexbuilder: builders.indexes,
19✔
239
            checkbuilder: builders.checks,
19✔
240
            primarykeybuilder: builders.primaryKeys,
19✔
241
            uniqueconstraintbuilder: builders.uniqueConstraints,
19✔
242
          };
19✔
243

244
          // Add builder to appropriate array if it matches any type
245
          for (const [type, array] of Object.entries(builderMap)) {
19✔
246
            if (builderName.includes(type)) {
57✔
247
              array.push(builder);
19✔
248
              break;
19✔
249
            }
19✔
250
          }
57✔
251

252
          // Always add to extras array
253
          builders.extras.push(builder);
19✔
254
        });
19✔
255
      }
19✔
256
    }
19✔
257
  }
44✔
258

259
  return {
44✔
260
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
44!
261
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
44!
262
    ...builders,
44✔
263
  };
44✔
264
}
44✔
265

266
/**
267
 * Generates SQL statements for dropping tables and/or their sequences.
268
 *
269
 * @param tables - List of table names to generate DROP statements for.
270
 * @param options - Configuration object:
271
 *   - sequence: whether to drop associated sequences (default: true)
272
 *   - table: whether to drop tables themselves (default: true)
273
 * @returns Array of SQL statements for dropping the specified objects
274
 */
275
export function generateDropTableStatements(
1✔
276
  tables: string[],
4✔
277
  options?: { sequence: boolean; table: boolean },
4✔
278
): string[] {
4✔
279
  const dropStatements: string[] = [];
4✔
280
  const validOptions = options ?? { sequence: true, table: true };
4✔
281
  if (!validOptions.sequence && !validOptions.table) {
4!
282
    console.warn('No drop operations requested: both "table" and "sequence" options are false');
×
283
    return [];
×
284
  }
×
285
  tables.forEach((tableName) => {
4✔
286
    if (validOptions.table) {
6✔
287
      dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
6✔
288
    }
6✔
289
    if (validOptions.sequence) {
6✔
290
      dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
6✔
291
    }
6✔
292
  });
4✔
293

294
  return dropStatements;
4✔
295
}
4✔
296

297
type AliasColumnMap = Record<string, AnyColumn>;
298

299
function mapSelectTableToAlias(
8✔
300
  table: MySqlTable,
8✔
301
  uniqPrefix: string,
8✔
302
  aliasMap: AliasColumnMap,
8✔
303
): any {
8✔
304
  const { columns, tableName } = getTableMetadata(table);
8✔
305
  const selectionsTableFields: Record<string, unknown> = {};
8✔
306
  Object.keys(columns).forEach((name) => {
8✔
307
    const column = columns[name] as AnyColumn;
31✔
308
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
31✔
309
    const fieldAlias = sql.raw(uniqName);
31✔
310
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
31✔
311
    aliasMap[uniqName] = column;
31✔
312
  });
8✔
313
  return selectionsTableFields;
8✔
314
}
8✔
315

316
function isDrizzleColumn(column: any): boolean {
36✔
317
  return column && typeof column === "object" && "table" in column;
36✔
318
}
36✔
319

320
export function mapSelectAllFieldsToAlias(
1✔
321
  selections: any,
44✔
322
  name: string,
44✔
323
  uniqName: string,
44✔
324
  fields: any,
44✔
325
  aliasMap: AliasColumnMap,
44✔
326
): any {
44✔
327
  if (isTable(fields)) {
44✔
328
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
8✔
329
  } else if (isDrizzleColumn(fields)) {
44✔
330
    const column = fields as Column;
20✔
331
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
20✔
332
    let aliasName = sql.raw(uniqAliasName);
20✔
333
    selections[name] = sql`${column} as \`${aliasName}\``;
20✔
334
    aliasMap[uniqAliasName] = column;
20✔
335
  } else if (isSQLWrapper(fields)) {
36✔
336
    selections[name] = fields;
7✔
337
  } else {
16✔
338
    const innerSelections: any = {};
9✔
339
    Object.entries(fields).forEach(([iname, ifields]) => {
9✔
340
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
18✔
341
    });
9✔
342
    selections[name] = innerSelections;
9✔
343
  }
9✔
344
  return selections;
44✔
345
}
44✔
346
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
1✔
347
  fields: TSelection,
9✔
348
): { selections: TSelection; aliasMap: AliasColumnMap } {
9✔
349
  if (!fields) {
9!
350
    throw new Error("fields is empty");
×
351
  }
×
352
  const aliasMap: AliasColumnMap = {};
9✔
353
  const selections: any = {};
9✔
354
  Object.entries(fields).forEach(([name, fields]) => {
9✔
355
    mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);
26✔
356
  });
9✔
357
  return { selections, aliasMap };
9✔
358
}
9✔
359

360
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
148✔
361
  const isSQL =
148✔
362
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
148✔
363
  if (isSQL) {
148✔
364
    const sql = value as SQL;
114✔
365
    const queryChunks = sql.queryChunks;
114✔
366
    if (queryChunks.length > 3) {
114✔
367
      const aliasNameChunk = queryChunks[queryChunks.length - 2];
100✔
368
      if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
100✔
369
        const aliasNameChunkSql = aliasNameChunk as SQL;
100✔
370
        if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
100✔
371
          const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
100✔
372
          if ("value" in queryChunksStringChunc) {
100✔
373
            const values = (queryChunksStringChunc as StringChunk).value;
100✔
374
            if (values && values.length === 1) {
100✔
375
              return values[0];
100✔
376
            }
100✔
377
          }
100✔
378
        }
100✔
379
      }
100✔
380
    }
100✔
381
  }
114✔
382
  return undefined;
48✔
383
}
48✔
384

385
function transformValue(
100✔
386
  value: unknown,
100✔
387
  alias: string,
100✔
388
  aliasMap: Record<string, AnyColumn>,
100✔
389
): unknown {
100✔
390
  const column = aliasMap[alias];
100✔
391
  if (!column) return value;
100!
392

393
  let customColumn = column as MySqlCustomColumn<any>;
100✔
394
  // @ts-ignore
395
  const fromDriver = customColumn?.mapFrom;
100✔
396
  if (fromDriver && value !== null && value !== undefined) {
100✔
397
    return fromDriver(value);
18✔
398
  }
18✔
399
  return value;
82✔
400
}
82✔
401

402
function transformObject(
51✔
403
  obj: Record<string, unknown>,
51✔
404
  selections: Record<string, unknown>,
51✔
405
  aliasMap: Record<string, AnyColumn>,
51✔
406
): Record<string, unknown> {
51✔
407
  const result: Record<string, unknown> = {};
51✔
408

409
  for (const [key, value] of Object.entries(obj)) {
51✔
410
    const selection = selections[key];
148✔
411
    const alias = getAliasFromDrizzleAlias(selection);
148✔
412
    if (alias && aliasMap[alias]) {
148✔
413
      result[key] = transformValue(value, alias, aliasMap);
100✔
414
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
148✔
415
      result[key] = transformObject(
34✔
416
        value as Record<string, unknown>,
34✔
417
        selection as Record<string, unknown>,
34✔
418
        aliasMap,
34✔
419
      );
34✔
420
    } else {
48✔
421
      result[key] = value;
14✔
422
    }
14✔
423
  }
148✔
424

425
  return result;
51✔
426
}
51✔
427

428
export function applyFromDriverTransform<T, TSelection>(
1✔
429
  rows: T[],
9✔
430
  selections: TSelection,
9✔
431
  aliasMap: Record<string, AnyColumn>,
9✔
432
): T[] {
9✔
433
  return rows.map((row) => {
9✔
434
    const transformed = transformObject(
17✔
435
      row as Record<string, unknown>,
17✔
436
      selections as Record<string, unknown>,
17✔
437
      aliasMap,
17✔
438
    ) as Record<string, unknown>;
17✔
439

440
    return processNullBranches(transformed) as unknown as T;
17✔
441
  });
9✔
442
}
9✔
443

444
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
55✔
445
  if (obj === null || typeof obj !== "object") {
55!
446
    return obj;
×
447
  }
×
448

449
  // Skip built-in objects like Date, Array, etc.
450
  if (obj.constructor && obj.constructor.name !== "Object") {
55✔
451
    return obj;
4✔
452
  }
4✔
453

454
  const result: Record<string, unknown> = {};
51✔
455
  let allNull = true;
51✔
456

457
  for (const [key, value] of Object.entries(obj)) {
55✔
458
    if (value === null || value === undefined) {
148✔
459
      result[key] = null;
5✔
460
      continue;
5✔
461
    }
5✔
462

463
    if (typeof value === "object") {
148✔
464
      const processed = processNullBranches(value as Record<string, unknown>);
38✔
465
      result[key] = processed;
38✔
466
      if (processed !== null) {
38✔
467
        allNull = false;
37✔
468
      }
37✔
469
    } else {
148✔
470
      result[key] = value;
105✔
471
      allNull = false;
105✔
472
    }
105✔
473
  }
148✔
474

475
  return allNull ? null : result;
55✔
476
}
55✔
477

478
export function formatLimitOffset(limitOrOffset: number): number {
1✔
479
  if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
5✔
480
    throw new Error("limitOrOffset must be a valid number");
2✔
481
  }
2✔
482
  return sql.raw(`${limitOrOffset}`) as unknown as number;
3✔
483
}
3✔
484

485
export function nextVal(sequenceName: string): number {
1✔
486
  return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;
3✔
487
}
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