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

vzakharchenko / forge-sql-orm / 17378635825

01 Sep 2025 01:10PM UTC coverage: 86.81% (-0.009%) from 86.819%
17378635825

push

github

vzakharchenko
fixed compatibility with moment library

265 of 327 branches covered (81.04%)

Branch coverage included in aggregate %.

10 of 11 new or added lines in 2 files covered. (90.91%)

1104 of 1250 relevant lines covered (88.32%)

11.14 hits per line

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

90.67
/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!
NEW
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
 * Gets primary keys from the schema.
79
 * @template T - The type of the table schema
80
 * @param {T} table - The table schema
81
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
82
 */
83
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
1✔
84
  const { columns, primaryKeys } = getTableMetadata(table);
14✔
85

86
  // First try to find primary keys in columns
87
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
14✔
88
    string,
89
    AnyColumn,
90
  ][];
91

92
  if (columnPrimaryKeys.length > 0) {
14✔
93
    return columnPrimaryKeys;
10✔
94
  }
10✔
95

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

101
    primaryKeys.forEach((primaryKeyBuilder) => {
4✔
102
      // Get primary key columns from each builder
103
      Object.entries(columns)
4✔
104
        .filter(([, column]) => {
4✔
105
          // @ts-ignore - PrimaryKeyBuilder has internal columns property
106
          return primaryKeyBuilder.columns.includes(column);
8✔
107
        })
4✔
108
        .forEach(([name, column]) => {
4✔
109
          primaryKeyColumns.add([name, column]);
4✔
110
        });
4✔
111
    });
4✔
112

113
    return Array.from(primaryKeyColumns);
4✔
114
  }
4!
115

116
  return [];
×
117
}
×
118

119
/**
120
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
121
 * @param table - The table schema
122
 * @param foreignKeysSymbol - Symbol for foreign keys
123
 * @param extraSymbol - Symbol for extra configuration
124
 * @returns Array of foreign key builders
125
 */
126
function processForeignKeys(
41✔
127
  table: AnyMySqlTable,
41✔
128
  foreignKeysSymbol: symbol | undefined,
41✔
129
  extraSymbol: symbol | undefined,
41✔
130
): ForeignKeyBuilder[] {
41✔
131
  const foreignKeys: ForeignKeyBuilder[] = [];
41✔
132

133
  // Process foreign keys from foreignKeysSymbol
134
  if (foreignKeysSymbol) {
41✔
135
    // @ts-ignore
136
    const fkArray: any[] = table[foreignKeysSymbol];
41✔
137
    if (fkArray) {
41✔
138
      fkArray.forEach((fk) => {
41✔
139
        if (fk.reference) {
×
140
          const item = fk.reference(fk);
×
141
          foreignKeys.push(item);
×
142
        }
×
143
      });
41✔
144
    }
41✔
145
  }
41✔
146

147
  // Process foreign keys from extraSymbol
148
  if (extraSymbol) {
41✔
149
    // @ts-ignore
150
    const extraConfigBuilder = table[extraSymbol];
41✔
151
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
41✔
152
      const configBuilderData = extraConfigBuilder(table);
16✔
153
      if (configBuilderData) {
16✔
154
        const configBuilders = Array.isArray(configBuilderData)
16✔
155
          ? configBuilderData
16!
156
          : Object.values(configBuilderData).map(
×
157
              (item) => (item as ConfigBuilderData).value ?? item,
×
158
            );
×
159

160
        configBuilders.forEach((builder) => {
16✔
161
          if (!builder?.constructor) return;
16!
162

163
          const builderName = builder.constructor.name.toLowerCase();
16✔
164
          if (builderName.includes("foreignkeybuilder")) {
16!
165
            foreignKeys.push(builder);
×
166
          }
×
167
        });
16✔
168
      }
16✔
169
    }
16✔
170
  }
41✔
171

172
  return foreignKeys;
41✔
173
}
41✔
174

175
/**
176
 * Extracts table metadata from the schema.
177
 * @param {AnyMySqlTable} table - The table schema
178
 * @returns {MetadataInfo} Object containing table metadata
179
 */
180
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
1✔
181
  const symbols = Object.getOwnPropertySymbols(table);
41✔
182
  const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
41✔
183
  const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
41✔
184
  const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
41✔
185
  const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
41✔
186

187
  // Initialize builders arrays
188
  const builders = {
41✔
189
    indexes: [] as AnyIndexBuilder[],
41✔
190
    checks: [] as CheckBuilder[],
41✔
191
    foreignKeys: [] as ForeignKeyBuilder[],
41✔
192
    primaryKeys: [] as PrimaryKeyBuilder[],
41✔
193
    uniqueConstraints: [] as UniqueConstraintBuilder[],
41✔
194
    extras: [] as any[],
41✔
195
  };
41✔
196

197
  // Process foreign keys
198
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
41✔
199

200
  // Process extra configuration if available
201
  if (extraSymbol) {
41✔
202
    // @ts-ignore
203
    const extraConfigBuilder = table[extraSymbol];
41✔
204
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
41✔
205
      const configBuilderData = extraConfigBuilder(table);
16✔
206
      if (configBuilderData) {
16✔
207
        // Convert configBuilderData to array if it's an object
208
        const configBuilders = Array.isArray(configBuilderData)
16✔
209
          ? configBuilderData
16!
210
          : Object.values(configBuilderData).map(
×
211
              (item) => (item as ConfigBuilderData).value ?? item,
×
212
            );
×
213

214
        // Process each builder
215
        configBuilders.forEach((builder) => {
16✔
216
          if (!builder?.constructor) return;
16!
217

218
          const builderName = builder.constructor.name.toLowerCase();
16✔
219

220
          // Map builder types to their corresponding arrays
221
          const builderMap = {
16✔
222
            indexbuilder: builders.indexes,
16✔
223
            checkbuilder: builders.checks,
16✔
224
            primarykeybuilder: builders.primaryKeys,
16✔
225
            uniqueconstraintbuilder: builders.uniqueConstraints,
16✔
226
          };
16✔
227

228
          // Add builder to appropriate array if it matches any type
229
          for (const [type, array] of Object.entries(builderMap)) {
16✔
230
            if (builderName.includes(type)) {
48✔
231
              array.push(builder);
16✔
232
              break;
16✔
233
            }
16✔
234
          }
48✔
235

236
          // Always add to extras array
237
          builders.extras.push(builder);
16✔
238
        });
16✔
239
      }
16✔
240
    }
16✔
241
  }
41✔
242

243
  return {
41✔
244
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
41!
245
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
41!
246
    ...builders,
41✔
247
  };
41✔
248
}
41✔
249

250
/**
251
 * Generates SQL statements for dropping tables and/or their sequences.
252
 *
253
 * @param tables - List of table names to generate DROP statements for.
254
 * @param options - Configuration object:
255
 *   - sequence: whether to drop associated sequences (default: true)
256
 *   - table: whether to drop tables themselves (default: true)
257
 * @returns Array of SQL statements for dropping the specified objects
258
 */
259
export function generateDropTableStatements(
1✔
260
  tables: string[],
4✔
261
  options?: { sequence: boolean; table: boolean },
4✔
262
): string[] {
4✔
263
  const dropStatements: string[] = [];
4✔
264
  const validOptions = options ?? { sequence: true, table: true };
4✔
265
  if (!validOptions.sequence && !validOptions.table) {
4!
266
    console.warn('No drop operations requested: both "table" and "sequence" options are false');
×
267
    return [];
×
268
  }
×
269
  tables.forEach((tableName) => {
4✔
270
    if (validOptions.table) {
6✔
271
      dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
6✔
272
    }
6✔
273
    if (validOptions.sequence) {
6✔
274
      dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
6✔
275
    }
6✔
276
  });
4✔
277

278
  return dropStatements;
4✔
279
}
4✔
280

281
type AliasColumnMap = Record<string, AnyColumn>;
282

283
function mapSelectTableToAlias(
6✔
284
  table: MySqlTable,
6✔
285
  uniqPrefix: string,
6✔
286
  aliasMap: AliasColumnMap,
6✔
287
): any {
6✔
288
  const { columns, tableName } = getTableMetadata(table);
6✔
289
  const selectionsTableFields: Record<string, unknown> = {};
6✔
290
  Object.keys(columns).forEach((name) => {
6✔
291
    const column = columns[name] as AnyColumn;
23✔
292
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
23✔
293
    const fieldAlias = sql.raw(uniqName);
23✔
294
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
23✔
295
    aliasMap[uniqName] = column;
23✔
296
  });
6✔
297
  return selectionsTableFields;
6✔
298
}
6✔
299

300
function isDrizzleColumn(column: any): boolean {
26✔
301
  return column && typeof column === "object" && "table" in column;
26✔
302
}
26✔
303

304
export function mapSelectAllFieldsToAlias(
1✔
305
  selections: any,
32✔
306
  name: string,
32✔
307
  uniqName: string,
32✔
308
  fields: any,
32✔
309
  aliasMap: AliasColumnMap,
32✔
310
): any {
32✔
311
  if (isTable(fields)) {
32✔
312
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
6✔
313
  } else if (isDrizzleColumn(fields)) {
32✔
314
    const column = fields as Column;
14✔
315
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
14✔
316
    let aliasName = sql.raw(uniqAliasName);
14✔
317
    selections[name] = sql`${column} as \`${aliasName}\``;
14✔
318
    aliasMap[uniqAliasName] = column;
14✔
319
  } else if (isSQLWrapper(fields)) {
26✔
320
    selections[name] = fields;
5✔
321
  } else {
12✔
322
    const innerSelections: any = {};
7✔
323
    Object.entries(fields).forEach(([iname, ifields]) => {
7✔
324
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
14✔
325
    });
7✔
326
    selections[name] = innerSelections;
7✔
327
  }
7✔
328
  return selections;
32✔
329
}
32✔
330
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
1✔
331
  fields: TSelection,
6✔
332
): { selections: TSelection; aliasMap: AliasColumnMap } {
6✔
333
  if (!fields) {
6!
334
    throw new Error("fields is empty");
×
335
  }
×
336
  const aliasMap: AliasColumnMap = {};
6✔
337
  const selections: any = {};
6✔
338
  Object.entries(fields).forEach(([name, fields]) => {
6✔
339
    mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);
18✔
340
  });
6✔
341
  return { selections, aliasMap };
6✔
342
}
6✔
343

344
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
110✔
345
  const isSQL =
110✔
346
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
110✔
347
  if (isSQL) {
110✔
348
    const sql = value as SQL;
84✔
349
    const queryChunks = sql.queryChunks;
84✔
350
    if (queryChunks.length > 3) {
84✔
351
      const aliasNameChunk = queryChunks[queryChunks.length - 2];
74✔
352
      if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
74✔
353
        const aliasNameChunkSql = aliasNameChunk as SQL;
74✔
354
        if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
74✔
355
          const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
74✔
356
          if ("value" in queryChunksStringChunc) {
74✔
357
            const values = (queryChunksStringChunc as StringChunk).value;
74✔
358
            if (values && values.length === 1) {
74✔
359
              return values[0];
74✔
360
            }
74✔
361
          }
74✔
362
        }
74✔
363
      }
74✔
364
    }
74✔
365
  }
84✔
366
  return undefined;
36✔
367
}
36✔
368

369
function transformValue(
74✔
370
  value: unknown,
74✔
371
  alias: string,
74✔
372
  aliasMap: Record<string, AnyColumn>,
74✔
373
): unknown {
74✔
374
  const column = aliasMap[alias];
74✔
375
  if (!column) return value;
74!
376

377
  let customColumn = column as MySqlCustomColumn<any>;
74✔
378
  // @ts-ignore
379
  const fromDriver = customColumn?.mapFrom;
74✔
380
  if (fromDriver && value !== null && value !== undefined) {
74✔
381
    return fromDriver(value);
14✔
382
  }
14✔
383
  return value;
60✔
384
}
60✔
385

386
function transformObject(
38✔
387
  obj: Record<string, unknown>,
38✔
388
  selections: Record<string, unknown>,
38✔
389
  aliasMap: Record<string, AnyColumn>,
38✔
390
): Record<string, unknown> {
38✔
391
  const result: Record<string, unknown> = {};
38✔
392

393
  for (const [key, value] of Object.entries(obj)) {
38✔
394
    const selection = selections[key];
110✔
395
    const alias = getAliasFromDrizzleAlias(selection);
110✔
396
    if (alias && aliasMap[alias]) {
110✔
397
      result[key] = transformValue(value, alias, aliasMap);
74✔
398
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
110✔
399
      result[key] = transformObject(
26✔
400
        value as Record<string, unknown>,
26✔
401
        selection as Record<string, unknown>,
26✔
402
        aliasMap,
26✔
403
      );
26✔
404
    } else {
36✔
405
      result[key] = value;
10✔
406
    }
10✔
407
  }
110✔
408

409
  return result;
38✔
410
}
38✔
411

412
export function applyFromDriverTransform<T, TSelection>(
1✔
413
  rows: T[],
6✔
414
  selections: TSelection,
6✔
415
  aliasMap: Record<string, AnyColumn>,
6✔
416
): T[] {
6✔
417
  return rows.map((row) => {
6✔
418
    const transformed = transformObject(
12✔
419
      row as Record<string, unknown>,
12✔
420
      selections as Record<string, unknown>,
12✔
421
      aliasMap,
12✔
422
    ) as Record<string, unknown>;
12✔
423

424
    return processNullBranches(transformed) as unknown as T;
12✔
425
  });
6✔
426
}
6✔
427

428
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
42✔
429
  if (obj === null || typeof obj !== "object") {
42!
430
    return obj;
×
431
  }
×
432

433
  // Skip built-in objects like Date, Array, etc.
434
  if (obj.constructor && obj.constructor.name !== "Object") {
42✔
435
    return obj;
4✔
436
  }
4✔
437

438
  const result: Record<string, unknown> = {};
38✔
439
  let allNull = true;
38✔
440

441
  for (const [key, value] of Object.entries(obj)) {
42✔
442
    if (value === null || value === undefined) {
110✔
443
      result[key] = null;
5✔
444
      continue;
5✔
445
    }
5✔
446

447
    if (typeof value === "object") {
110✔
448
      const processed = processNullBranches(value as Record<string, unknown>);
30✔
449
      result[key] = processed;
30✔
450
      if (processed !== null) {
30✔
451
        allNull = false;
29✔
452
      }
29✔
453
    } else {
110✔
454
      result[key] = value;
75✔
455
      allNull = false;
75✔
456
    }
75✔
457
  }
110✔
458

459
  return allNull ? null : result;
42✔
460
}
42✔
461

462
export function formatLimitOffset(limitOrOffset: number): number {
1✔
463
  if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
5✔
464
    throw new Error("limitOrOffset must be a valid number");
2✔
465
  }
2✔
466
  return sql.raw(`${limitOrOffset}`) as unknown as number;
3✔
467
}
3✔
468

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