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

rogerpadilla / uql / 20685289522

04 Jan 2026 12:49AM UTC coverage: 95.658% (-0.1%) from 95.778%
20685289522

push

github

rogerpadilla
chore(release): publish

 - @uql/core@3.6.0

1303 of 1434 branches covered (90.86%)

Branch coverage included in aggregate %.

3434 of 3518 relevant lines covered (97.61%)

234.32 hits per line

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

88.75
/packages/core/src/migrate/migrator.ts
1
import { readdir } from 'node:fs/promises';
2
import { basename, extname, join } from 'node:path';
3
import { pathToFileURL } from 'node:url';
4
import { getEntities, getMeta } from '../entity/index.js';
5
import type {
6
  Dialect,
7
  LoggingOptions,
8
  Migration,
9
  MigrationDefinition,
10
  MigrationResult,
11
  MigrationStorage,
12
  MigratorOptions,
13
  MongoQuerier,
14
  NamingStrategy,
15
  Querier,
16
  QuerierPool,
17
  SchemaDiff,
18
  SchemaGenerator,
19
  SchemaIntrospector,
20
  Type,
21
} from '../type/index.js';
22
import { isSqlQuerier } from '../type/index.js';
23
import { LoggerWrapper } from '../util/index.js';
24
import {
25
  MariadbSchemaGenerator,
26
  MongoSchemaGenerator,
27
  MysqlSchemaGenerator,
28
  PostgresSchemaGenerator,
29
  SqliteSchemaGenerator,
30
} from './generator/index.js';
31
import {
32
  MariadbSchemaIntrospector,
33
  MongoSchemaIntrospector,
34
  MysqlSchemaIntrospector,
35
  PostgresSchemaIntrospector,
36
  SqliteSchemaIntrospector,
37
} from './introspection/index.js';
38
import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
39

40
/**
41
 * Main class for managing database migrations
42
 */
43
export class Migrator {
44
  public readonly storage: MigrationStorage;
45
  public readonly migrationsPath: string;
46
  private _logger: LoggerWrapper;
47
  public get logger(): LoggerWrapper {
48
    return this._logger;
49✔
49
  }
50
  public set logger(value: LoggingOptions) {
51
    this._logger = new LoggerWrapper(value);
2✔
52
  }
53
  private readonly _entities?: Type<unknown>[];
54

55
  public get entities(): Type<unknown>[] {
56
    return this._entities ?? getEntities();
20✔
57
  }
58
  public readonly dialect: Dialect;
59
  public schemaGenerator?: SchemaGenerator;
60
  public schemaIntrospector?: SchemaIntrospector;
61

62
  constructor(
63
    private readonly pool: QuerierPool,
73✔
64
    options: MigratorOptions = {},
73✔
65
  ) {
66
    this.dialect = options.dialect ?? pool.dialect ?? 'postgres';
73!
67
    this.storage =
73✔
68
      options.storage ??
97✔
69
      new DatabaseMigrationStorage(pool, {
70
        tableName: options.tableName,
71
      });
72
    this.migrationsPath = options.migrationsPath ?? './migrations';
73✔
73
    this._logger = new LoggerWrapper(options.logger, options.slowQueryThreshold);
73✔
74
    this._entities = options.entities;
73✔
75
    this.schemaIntrospector = this.createIntrospector();
73✔
76
    this.schemaGenerator = options.schemaGenerator ?? this.createGenerator(options.namingStrategy);
73✔
77
  }
78

79
  /**
80
   * Set the schema generator for DDL operations
81
   */
82
  setSchemaGenerator(generator: SchemaGenerator): void {
83
    this.schemaGenerator = generator;
2✔
84
  }
85

86
  protected createIntrospector(): SchemaIntrospector | undefined {
87
    switch (this.dialect) {
73✔
88
      case 'postgres':
89
        return new PostgresSchemaIntrospector(this.pool);
62✔
90
      case 'mysql':
91
        return new MysqlSchemaIntrospector(this.pool);
2✔
92
      case 'mariadb':
93
        return new MariadbSchemaIntrospector(this.pool);
1✔
94
      case 'sqlite':
95
        return new SqliteSchemaIntrospector(this.pool);
1✔
96
      case 'mongodb':
97
        return new MongoSchemaIntrospector(this.pool);
4✔
98
      default:
99
        return undefined;
3✔
100
    }
101
  }
102

103
  protected createGenerator(namingStrategy?: NamingStrategy): SchemaGenerator | undefined {
104
    switch (this.dialect) {
70✔
105
      case 'postgres':
106
        return new PostgresSchemaGenerator(namingStrategy);
59✔
107
      case 'mysql':
108
        return new MysqlSchemaGenerator(namingStrategy);
2✔
109
      case 'mariadb':
110
        return new MariadbSchemaGenerator(namingStrategy);
1✔
111
      case 'sqlite':
112
        return new SqliteSchemaGenerator(namingStrategy);
1✔
113
      case 'mongodb':
114
        return new MongoSchemaGenerator(namingStrategy);
4✔
115
      default:
116
        return undefined;
3✔
117
    }
118
  }
119

120
  /**
121
   * Get the SQL dialect
122
   */
123
  getDialect(): Dialect {
124
    return this.dialect;
2✔
125
  }
126

127
  /**
128
   * Get all discovered migrations from the migrations directory
129
   */
130
  async getMigrations(): Promise<Migration[]> {
131
    const files = await this.getMigrationFiles();
×
132
    const migrations: Migration[] = [];
×
133

134
    for (const file of files) {
×
135
      const migration = await this.loadMigration(file);
×
136
      if (migration) {
×
137
        migrations.push(migration);
×
138
      }
139
    }
140

141
    // Sort by name (which typically includes timestamp)
142
    return migrations.sort((a: any, b: any) => a.name.localeCompare(b.name));
×
143
  }
144

145
  /**
146
   * Get list of pending migrations (not yet executed)
147
   */
148
  async pending(): Promise<Migration[]> {
149
    const [migrations, executed] = await Promise.all([this.getMigrations(), this.storage.executed()]);
7✔
150

151
    const executedSet = new Set(executed);
7✔
152
    return migrations.filter((m: any) => !executedSet.has(m.name));
18✔
153
  }
154

155
  /**
156
   * Get list of executed migrations
157
   */
158
  async executed(): Promise<string[]> {
159
    return this.storage.executed();
2✔
160
  }
161

162
  /**
163
   * Run all pending migrations
164
   */
165
  async up(options: { to?: string; step?: number } = {}): Promise<MigrationResult[]> {
4✔
166
    const pendingMigrations = await this.pending();
4✔
167
    const results: MigrationResult[] = [];
4✔
168

169
    let migrationsToRun = pendingMigrations;
4✔
170

171
    if (options.to) {
4✔
172
      const toIndex = migrationsToRun.findIndex((m: any) => m.name === options.to);
2✔
173
      if (toIndex === -1) {
2✔
174
        throw new Error(`Migration '${options.to}' not found`);
1✔
175
      }
176
      migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
1✔
177
    }
178

179
    if (options.step !== undefined) {
3!
180
      migrationsToRun = migrationsToRun.slice(0, options.step);
×
181
    }
182

183
    for (const migration of migrationsToRun) {
3✔
184
      const result = await this.runMigration(migration, 'up');
6✔
185
      results.push(result);
6✔
186

187
      if (!result.success) {
6✔
188
        break; // Stop on first failure
1✔
189
      }
190
    }
191

192
    return results;
3✔
193
  }
194

195
  /**
196
   * Rollback migrations
197
   */
198
  async down(options: { to?: string; step?: number } = {}): Promise<MigrationResult[]> {
4✔
199
    const [migrations, executed] = await Promise.all([this.getMigrations(), this.storage.executed()]);
4✔
200

201
    const executedSet = new Set(executed);
4✔
202
    const executedMigrations = migrations.filter((m: any) => executedSet.has(m.name)).reverse(); // Rollback in reverse order
9✔
203

204
    const results: MigrationResult[] = [];
4✔
205
    let migrationsToRun = executedMigrations;
4✔
206

207
    if (options.to) {
4✔
208
      const toIndex = migrationsToRun.findIndex((m: any) => m.name === options.to);
2✔
209
      if (toIndex === -1) {
2✔
210
        throw new Error(`Migration '${options.to}' not found`);
1✔
211
      }
212
      migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
1✔
213
    }
214

215
    if (options.step !== undefined) {
3✔
216
      migrationsToRun = migrationsToRun.slice(0, options.step);
1✔
217
    }
218

219
    for (const migration of migrationsToRun) {
3✔
220
      const result = await this.runMigration(migration, 'down');
4✔
221
      results.push(result);
4✔
222

223
      if (!result.success) {
4✔
224
        break; // Stop on first failure
1✔
225
      }
226
    }
227

228
    return results;
3✔
229
  }
230

231
  /**
232
   * Run a single migration within a transaction
233
   */
234
  public async runMigration(migration: Migration, direction: 'up' | 'down'): Promise<MigrationResult> {
235
    const startTime = Date.now();
11✔
236
    const querier = await this.pool.getQuerier();
11✔
237

238
    if (!isSqlQuerier(querier)) {
11✔
239
      await querier.release();
1✔
240
      throw new Error('Migrator requires a SQL-based querier');
1✔
241
    }
242

243
    try {
10✔
244
      this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
10✔
245

246
      await querier.beginTransaction();
11✔
247

248
      if (direction === 'up') {
10✔
249
        await migration.up(querier);
6✔
250
        // Log within the same transaction
251
        await this.storage.logWithQuerier(querier, migration.name);
5✔
252
      } else {
253
        await migration.down(querier);
4✔
254
        // Unlog within the same transaction
255
        await this.storage.unlogWithQuerier(querier, migration.name);
3✔
256
      }
257

258
      await querier.commitTransaction();
8✔
259

260
      const duration = Date.now() - startTime;
8✔
261
      this.logger.logMigration(
8✔
262
        `Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`,
8✔
263
      );
264

265
      return {
11✔
266
        name: migration.name,
267
        direction,
268
        duration,
269
        success: true,
270
      };
271
    } catch (error) {
272
      await querier.rollbackTransaction();
2✔
273

274
      const duration = Date.now() - startTime;
2✔
275
      this.logger.logError(`Migration ${migration.name} failed: ${(error as Error).message}`, error);
2✔
276

277
      return {
2✔
278
        name: migration.name,
279
        direction,
280
        duration,
281
        success: false,
282
        error: error as Error,
283
      };
284
    } finally {
285
      await querier.release();
10✔
286
    }
287
  }
288

289
  /**
290
   * Generate a new migration file
291
   */
292
  async generate(name: string): Promise<string> {
293
    const timestamp = this.getTimestamp();
1✔
294
    const fileName = `${timestamp}_${this.slugify(name)}.ts`;
1✔
295
    const filePath = join(this.migrationsPath, fileName);
1✔
296

297
    const content = this.generateMigrationContent(name);
1✔
298

299
    const { writeFile, mkdir } = await import('node:fs/promises');
1✔
300
    await mkdir(this.migrationsPath, { recursive: true });
1✔
301
    await writeFile(filePath, content, 'utf-8');
1✔
302

303
    this.logger.logInfo(`Created migration: ${filePath}`);
1✔
304
    return filePath;
1✔
305
  }
306

307
  /**
308
   * Generate a migration based on entity schema differences
309
   */
310
  async generateFromEntities(name: string): Promise<string> {
311
    if (!this.schemaGenerator) {
3✔
312
      throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
1✔
313
    }
314

315
    const diffs = await this.getDiffs();
2✔
316
    const upStatements: string[] = [];
2✔
317
    const downStatements: string[] = [];
2✔
318

319
    for (const diff of diffs) {
2✔
320
      if (diff.type === 'create') {
1!
321
        const entity = this.findEntityForTable(diff.tableName);
×
322
        if (entity) {
×
323
          upStatements.push(this.schemaGenerator.generateCreateTable(entity));
×
324
          downStatements.push(this.schemaGenerator.generateDropTable(entity));
×
325
        }
326
      } else if (diff.type === 'alter') {
1!
327
        const alterStatements = this.schemaGenerator.generateAlterTable(diff);
1✔
328
        upStatements.push(...alterStatements);
1✔
329

330
        const alterDownStatements = this.schemaGenerator.generateAlterTableDown(diff);
1✔
331
        downStatements.push(...alterDownStatements);
1✔
332
      }
333
    }
334

335
    if (upStatements.length === 0) {
2✔
336
      this.logger.logInfo('No schema changes detected.');
1✔
337
      return '';
1✔
338
    }
339

340
    const timestamp = this.getTimestamp();
1✔
341
    const fileName = `${timestamp}_${this.slugify(name)}.ts`;
1✔
342
    const filePath = join(this.migrationsPath, fileName);
1✔
343

344
    const content = this.generateMigrationContentWithStatements(name, upStatements, downStatements.reverse());
1✔
345

346
    const { writeFile, mkdir } = await import('node:fs/promises');
1✔
347
    await mkdir(this.migrationsPath, { recursive: true });
1✔
348
    await writeFile(filePath, content, 'utf-8');
1✔
349

350
    this.logger.logInfo(`Created migration from entities: ${filePath}`);
1✔
351
    return filePath;
1✔
352
  }
353

354
  /**
355
   * Get all schema differences between entities and database
356
   */
357
  async getDiffs(): Promise<SchemaDiff[]> {
358
    if (!this.schemaGenerator || !this.schemaIntrospector) {
6!
359
      throw new Error('Schema generator and introspector must be set');
×
360
    }
361

362
    const diffs: SchemaDiff[] = [];
6✔
363

364
    for (const entity of this.entities) {
6✔
365
      const meta = getMeta(entity);
10✔
366
      const tableName = this.schemaGenerator.resolveTableName(entity, meta);
10✔
367
      const currentSchema = await this.schemaIntrospector.getTableSchema(tableName);
10✔
368
      const diff = this.schemaGenerator.diffSchema(entity, currentSchema);
10✔
369
      if (diff) {
10!
370
        diffs.push(diff);
10✔
371
      }
372
    }
373

374
    return diffs;
6✔
375
  }
376

377
  protected findEntityForTable(tableName: string): Type<unknown> | undefined {
378
    for (const entity of this.entities) {
8✔
379
      const meta = getMeta(entity);
13✔
380
      const name = this.schemaGenerator.resolveTableName(entity, meta);
13✔
381
      if (name === tableName) {
13✔
382
        return entity;
8✔
383
      }
384
    }
385
    return undefined;
×
386
  }
387

388
  /**
389
   * Sync schema directly (for development only - not for production!)
390
   */
391
  async sync(options: { force?: boolean } = {}): Promise<void> {
3✔
392
    if (options.force) {
3✔
393
      return this.syncForce();
1✔
394
    }
395
    return this.autoSync({ safe: true });
2✔
396
  }
397

398
  /**
399
   * Drops and recreates all tables (Development only!)
400
   */
401
  public async syncForce(): Promise<void> {
402
    if (!this.schemaGenerator) {
3!
403
      throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
×
404
    }
405

406
    const querier = await this.pool.getQuerier();
3✔
407

408
    if (!isSqlQuerier(querier)) {
3✔
409
      await querier.release();
1✔
410
      throw new Error('Migrator requires a SQL-based querier');
1✔
411
    }
412

413
    try {
2✔
414
      await querier.beginTransaction();
2✔
415

416
      // Drop all tables first (in reverse order for foreign keys)
417
      for (const entity of [...this.entities].reverse()) {
2✔
418
        const dropSql = this.schemaGenerator.generateDropTable(entity);
2✔
419
        this.logger.logSchema(`Executing: ${dropSql}`);
2✔
420
        await querier.run(dropSql);
2✔
421
      }
422

423
      // Create all tables
424
      for (const entity of this.entities) {
2✔
425
        const createSql = this.schemaGenerator.generateCreateTable(entity);
2✔
426
        this.logger.logSchema(`Executing: ${createSql}`);
2✔
427
        await querier.run(createSql);
2✔
428
      }
429

430
      await querier.commitTransaction();
2✔
431
      this.logger.logSchema('Schema sync (force) completed');
2✔
432
    } catch (error) {
433
      await querier.rollbackTransaction();
×
434
      throw error;
×
435
    } finally {
436
      await querier.release();
2✔
437
    }
438
  }
439

440
  /**
441
   * Safely synchronizes the schema by only adding missing tables and columns.
442
   */
443
  async autoSync(options: { safe?: boolean; drop?: boolean; logging?: boolean } = {}): Promise<void> {
7✔
444
    if (!this.schemaGenerator || !this.schemaIntrospector) {
7✔
445
      throw new Error('Schema generator and introspector must be set');
1✔
446
    }
447

448
    const diffs = await this.getDiffs();
6✔
449
    const statements: string[] = [];
6✔
450

451
    for (const diff of diffs) {
6✔
452
      if (diff.type === 'create') {
9✔
453
        const entity = this.findEntityForTable(diff.tableName);
8✔
454
        if (entity) {
8!
455
          statements.push(this.schemaGenerator.generateCreateTable(entity));
8✔
456
        }
457
      } else if (diff.type === 'alter') {
1!
458
        const filteredDiff = this.filterDiff(diff, options);
1✔
459
        const alterStatements = this.schemaGenerator.generateAlterTable(filteredDiff);
1✔
460
        statements.push(...alterStatements);
1✔
461
      }
462
    }
463

464
    if (statements.length === 0) {
6✔
465
      if (options.logging) this.logger.logSchema('Schema is already in sync.');
1!
466
      return;
1✔
467
    }
468

469
    await this.executeSyncStatements(statements, options);
5✔
470
  }
471

472
  protected filterDiff(diff: SchemaDiff, options: { safe?: boolean; drop?: boolean }): SchemaDiff {
473
    const filteredDiff = { ...diff } as { -readonly [K in keyof SchemaDiff]: SchemaDiff[K] };
1✔
474
    if (options.safe !== false) {
1!
475
      // In safe mode, we only allow additions
476
      delete filteredDiff.columnsToDrop;
1✔
477
      delete filteredDiff.indexesToDrop;
1✔
478
      delete filteredDiff.foreignKeysToDrop;
1✔
479
    }
480
    if (!options.drop) {
1!
481
      delete filteredDiff.columnsToDrop;
1✔
482
    }
483
    return filteredDiff;
1✔
484
  }
485

486
  public async executeSyncStatements(statements: string[], options: { logging?: boolean }): Promise<void> {
487
    const querier = await this.pool.getQuerier();
7✔
488
    try {
7✔
489
      if (this.dialect === 'mongodb') {
7✔
490
        await this.executeMongoSyncStatements(statements, options, querier as MongoQuerier);
3✔
491
      } else {
492
        await this.executeSqlSyncStatements(statements, options, querier);
4✔
493
      }
494
      if (options.logging) this.logger.logSchema('Schema synchronization completed');
6✔
495
    } catch (error) {
496
      if (this.dialect !== 'mongodb' && isSqlQuerier(querier)) {
1!
497
        await querier.rollbackTransaction();
×
498
      }
499
      throw error;
1✔
500
    } finally {
501
      await querier.release();
7✔
502
    }
503
  }
504

505
  protected async executeMongoSyncStatements(
506
    statements: string[],
507
    options: { logging?: boolean },
508
    querier: MongoQuerier,
509
  ): Promise<void> {
510
    const db = querier.db;
3✔
511
    for (const stmt of statements) {
3✔
512
      const cmd = JSON.parse(stmt) as {
6✔
513
        action: string;
514
        name?: string;
515
        collection?: string;
516
        indexes?: { name: string; columns: string[]; unique?: boolean }[];
517
        key?: Record<string, number>;
518
        options?: any;
519
      };
520
      if (options.logging) this.logger.logSchema(`Executing MongoDB: ${stmt}`);
6✔
521

522
      const collectionName = cmd.name || cmd.collection;
6✔
523
      if (!collectionName) {
6✔
524
        throw new Error(`MongoDB command missing collection name: ${stmt}`);
1✔
525
      }
526
      const collection = db.collection(collectionName);
5✔
527

528
      if (cmd.action === 'createCollection') {
5✔
529
        await db.createCollection(cmd.name);
2✔
530
        if (cmd.indexes?.length) {
2!
531
          for (const idx of cmd.indexes) {
2✔
532
            const key = Object.fromEntries(idx.columns.map((c: string) => [c, 1]));
2✔
533
            await collection.createIndex(key, { unique: idx.unique, name: idx.name });
2✔
534
          }
535
        }
536
      } else if (cmd.action === 'dropCollection') {
3✔
537
        await collection.drop();
1✔
538
      } else if (cmd.action === 'createIndex') {
2✔
539
        await collection.createIndex(cmd.key, cmd.options);
1✔
540
      } else if (cmd.action === 'dropIndex') {
1!
541
        await collection.dropIndex(cmd.name);
1✔
542
      }
543
    }
544
  }
545

546
  public async executeSqlSyncStatements(
547
    statements: string[],
548
    options: { logging?: boolean },
549
    querier: Querier,
550
  ): Promise<void> {
551
    if (!isSqlQuerier(querier)) {
5✔
552
      throw new Error('Migrator requires a SQL-based querier for this dialect');
1✔
553
    }
554
    await querier.beginTransaction();
4✔
555
    for (const sql of statements) {
4✔
556
      if (options.logging) this.logger.logSchema(`Executing: ${sql}`);
11✔
557
      await querier.run(sql);
11✔
558
    }
559
    await querier.commitTransaction();
4✔
560
  }
561

562
  /**
563
   * Get migration status
564
   */
565
  async status(): Promise<{ pending: string[]; executed: string[] }> {
566
    const [pending, executed] = await Promise.all([this.pending().then((m) => m.map((x) => x.name)), this.executed()]);
4✔
567

568
    return { pending, executed };
2✔
569
  }
570

571
  /**
572
   * Get migration files from the migrations directory
573
   */
574
  public async getMigrationFiles(): Promise<string[]> {
575
    try {
3✔
576
      const files = await readdir(this.migrationsPath);
3✔
577
      return files
1✔
578
        .filter((f) => /\.(ts|js|mjs)$/.test(f))
4✔
579
        .filter((f) => !f.endsWith('.d.ts'))
3✔
580
        .sort();
581
    } catch (error) {
582
      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
2✔
583
        return [];
1✔
584
      }
585
      throw error;
1✔
586
    }
587
  }
588

589
  /**
590
   * Load a migration from a file
591
   */
592
  public async loadMigration(fileName: string): Promise<Migration | undefined> {
593
    const filePath = join(this.migrationsPath, fileName);
1✔
594
    const fileUrl = pathToFileURL(filePath).href;
1✔
595

596
    try {
1✔
597
      const module = await import(fileUrl);
1✔
598
      const migration = module.default ?? module;
×
599

600
      if (this.isMigration(migration)) {
1!
601
        return {
×
602
          name: this.getMigrationName(fileName),
603
          up: migration.up.bind(migration),
604
          down: migration.down.bind(migration),
605
        };
606
      }
607

608
      this.logger.logWarn(`Warning: ${fileName} is not a valid migration`);
×
609
      return undefined;
×
610
    } catch (error) {
611
      this.logger.logError(`Error loading migration ${fileName}: ${(error as Error).message}`, error);
1✔
612
      return undefined;
1✔
613
    }
614
  }
615

616
  /**
617
   * Check if an object is a valid migration
618
   */
619
  public isMigration(obj: unknown): obj is MigrationDefinition {
620
    return (
7✔
621
      typeof obj === 'object' &&
30✔
622
      obj !== undefined &&
623
      obj !== null &&
624
      typeof (obj as MigrationDefinition).up === 'function' &&
625
      typeof (obj as MigrationDefinition).down === 'function'
626
    );
627
  }
628

629
  /**
630
   * Extract migration name from filename
631
   */
632
  public getMigrationName(fileName: string): string {
633
    return basename(fileName, extname(fileName));
1✔
634
  }
635

636
  /**
637
   * Generate timestamp string for migration names
638
   */
639
  protected getTimestamp(): string {
640
    const now = new Date();
2✔
641
    return [
2✔
642
      now.getFullYear(),
643
      String(now.getMonth() + 1).padStart(2, '0'),
644
      String(now.getDate()).padStart(2, '0'),
645
      String(now.getHours()).padStart(2, '0'),
646
      String(now.getMinutes()).padStart(2, '0'),
647
      String(now.getSeconds()).padStart(2, '0'),
648
    ].join('');
649
  }
650

651
  /**
652
   * Convert a string to a slug for filenames
653
   */
654
  protected slugify(text: string): string {
655
    return text
2✔
656
      .toLowerCase()
657
      .replace(/[^a-z0-9]+/g, '_')
658
      .replace(/^_+|_+$/g, '');
659
  }
660

661
  /**
662
   * Generate migration file content
663
   */
664
  protected generateMigrationContent(name: string): string {
665
    return /*ts*/ `import type { SqlQuerier } from '@uql/migrate';
1✔
666

667
/**
668
 * Migration: ${name}
669
 * Created: ${new Date().toISOString()}
670
 */
671
export default {
672
  async up(querier: SqlQuerier): Promise<void> {
673
    // Add your migration logic here
674
    // Example:
675
    // await querier.run(\`
676
    //   CREATE TABLE "users" (
677
    //     "id" SERIAL PRIMARY KEY,
678
    //     "name" VARCHAR(255) NOT NULL,
679
    //     "email" VARCHAR(255) UNIQUE NOT NULL,
680
    //     "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
681
    //   )
682
    // \`);
683
  },
684

685
  async down(querier: SqlQuerier): Promise<void> {
686
    // Add your rollback logic here
687
    // Example:
688
    // await querier.run(\`DROP TABLE IF EXISTS "users"\`);
689
  },
690
};
691
`;
692
  }
693

694
  /**
695
   * Generate migration file content with SQL statements
696
   */
697
  protected generateMigrationContentWithStatements(
698
    name: string,
699
    upStatements: string[],
700
    downStatements: string[],
701
  ): string {
702
    const upSql = upStatements.map((s) => /*ts*/ `    await querier.run(\`${s}\`);`).join('\n');
1✔
703
    const downSql = downStatements.map((s) => /*ts*/ `    await querier.run(\`${s}\`);`).join('\n');
1✔
704

705
    return /*ts*/ `import type { SqlQuerier } from '@uql/migrate';
1✔
706

707
/**
708
 * Migration: ${name}
709
 * Created: ${new Date().toISOString()}
710
 * Generated from entity definitions
711
 */
712
export default {
713
  async up(querier: SqlQuerier): Promise<void> {
714
${upSql}
715
  },
716

717
  async down(querier: SqlQuerier): Promise<void> {
718
${downSql}
719
  },
720
};
721
`;
722
  }
723
}
724

725
/**
726
 * Helper function to define a migration with proper typing
727
 */
728
export function defineMigration(migration: MigrationDefinition): MigrationDefinition {
729
  return migration;
1✔
730
}
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