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

vzakharchenko / forge-sql-orm / 17372812591

01 Sep 2025 08:59AM UTC coverage: 86.819% (+0.1%) from 86.696%
17372812591

Pull #565

github

web-flow
Merge 1f63a5cf5 into 60685a987
Pull Request #565: Remove moment

267 of 329 branches covered (81.16%)

Branch coverage included in aggregate %.

18 of 23 new or added lines in 3 files covered. (78.26%)

1103 of 1249 relevant lines covered (88.31%)

22.27 hits per line

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

90.69
/src/utils/sqlUtils.ts
1
import { AnyColumn, Column, isTable, SQL, sql, StringChunk } from "drizzle-orm";
2✔
2
import { AnyMySqlTable, MySqlCustomColumn } from "drizzle-orm/mysql-core/index";
3
import { DateTime } from "luxon";
2✔
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";
2✔
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 => {
2✔
51
  let result: Date;
18✔
52
  if (value instanceof Date) {
18✔
53
    return value;
2✔
54
  }
2✔
55
  // 1. Try to parse using the provided format (strict mode)
56
  const dt = DateTime.fromFormat(value, format);
16✔
57
  if (dt.isValid) {
18✔
58
    result = dt.toJSDate();
8✔
59
  } else {
8✔
60
    // 2. Try to parse as ISO string
61
    const isoDt = DateTime.fromISO(value);
8✔
62
    if (isoDt.isValid) {
8!
NEW
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
  // 4. Ensure the result is a valid Date object
70
  if (isNaN(result.getTime())) {
18!
71
    result = new Date(value);
×
72
  }
✔
73
  return result;
16✔
74
};
16✔
75

76
/**
77
 * Gets primary keys from the schema.
78
 * @template T - The type of the table schema
79
 * @param {T} table - The table schema
80
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
81
 */
82
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
2✔
83
  const { columns, primaryKeys } = getTableMetadata(table);
28✔
84

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

91
  if (columnPrimaryKeys.length > 0) {
28✔
92
    return columnPrimaryKeys;
20✔
93
  }
20✔
94

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

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

112
    return Array.from(primaryKeyColumns);
8✔
113
  }
8!
114

115
  return [];
×
116
}
×
117

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

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

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

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

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

171
  return foreignKeys;
82✔
172
}
82✔
173

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

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

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

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

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

217
          const builderName = builder.constructor.name.toLowerCase();
32✔
218

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

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

235
          // Always add to extras array
236
          builders.extras.push(builder);
32✔
237
        });
32✔
238
      }
32✔
239
    }
32✔
240
  }
82✔
241

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

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

277
  return dropStatements;
8✔
278
}
8✔
279

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

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

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

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

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

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

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

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

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

408
  return result;
76✔
409
}
76✔
410

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

423
    return processNullBranches(transformed) as unknown as T;
24✔
424
  });
12✔
425
}
12✔
426

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

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

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

440
  for (const [key, value] of Object.entries(obj)) {
84✔
441
    if (value === null || value === undefined) {
220✔
442
      result[key] = null;
10✔
443
      continue;
10✔
444
    }
10✔
445

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

458
  return allNull ? null : result;
84✔
459
}
84✔
460

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

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