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

rogerpadilla / uql / 23875832875

01 Apr 2026 11:27PM UTC coverage: 94.888% (-0.03%) from 94.915%
23875832875

push

github

rogerpadilla
chore: update gitHead in package.json to reflect latest commit

2987 of 3313 branches covered (90.16%)

Branch coverage included in aggregate %.

5273 of 5392 relevant lines covered (97.79%)

345.18 hits per line

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

84.14
/packages/uql-orm/src/migrate/cli.ts
1
#!/usr/bin/env node
2

3
import * as fs from 'node:fs';
4
import * as path from 'node:path';
5
import type { AbstractDialect } from '../dialect/index.js';
6
import { type Drift, type DriftReport, SchemaASTBuilder } from '../schema/index.js';
7
import type { Config, MigratorOptions, NamingStrategy } from '../type/index.js';
8
import { assertCliConfig } from './assertCliConfig.js';
9
import { loadConfig } from './cli-config.js';
10
import { createEntityCodeGenerator } from './codegen/entityCodeGenerator.js';
11
import { detectDrift } from './drift/driftDetector.js';
12
import { Migrator } from './migrator.js';
13
import { createSchemaGenerator } from './schemaGenerator.js';
14
import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
15
import { createSchemaSync } from './sync/schemaSync.js';
16

17
/** Sync helper for SQL dialects only; returns `undefined` for MongoDB — use {@link createSchemaGeneratorAsync}. */
18
export function getSchemaGenerator(dialect: AbstractDialect, namingStrategy?: NamingStrategy) {
19
  return createSchemaGenerator(dialect, namingStrategy);
6✔
20
}
21

22
export { createSchemaGeneratorAsync };
23

24
export async function main(args = process.argv.slice(2)) {
20✔
25
  let customPath: string | undefined;
26
  const filteredArgs: string[] = [];
20✔
27

28
  for (let i = 0; i < args.length; i++) {
20✔
29
    if ((args[i] === '--config' || args[i] === '-c') && args[i + 1]) {
26✔
30
      customPath = args[++i];
2✔
31
    } else {
32
      filteredArgs.push(args[i]);
24✔
33
    }
34
  }
35

36
  const command = filteredArgs[0];
20✔
37

38
  if (!command || command === '--help' || command === '-h') {
20✔
39
    printHelp();
3✔
40
    return;
3✔
41
  }
42

43
  try {
17✔
44
    const config = await loadConfig(customPath);
17✔
45
    assertCliConfig(config);
17✔
46

47
    const dialectName = config.pool.dialect.dialectName ?? 'postgres';
17!
48

49
    const options: MigratorOptions = {
20✔
50
      migrationsPath: config.migrationsPath ?? './migrations',
36✔
51
      tableName: config.tableName,
52
      logger: console.log,
53
      entities: config.entities,
54
      namingStrategy: config.namingStrategy,
55
    };
56

57
    const migrator = new Migrator(config.pool, options);
20✔
58
    if (!migrator.schemaGenerator) {
20✔
59
      const generator = await createSchemaGeneratorAsync(config.pool.dialect, config.namingStrategy);
16✔
60
      if (!generator) {
16!
61
        throw new TypeError(`Could not find a schema generator for dialect: ${dialectName}`);
×
62
      }
63
      migrator.setSchemaGenerator(generator);
16✔
64
    }
65

66
    switch (command) {
16!
67
      case 'up':
68
        await runUp(migrator, filteredArgs.slice(1));
5✔
69
        break;
3✔
70
      case 'down':
71
        await runDown(migrator, filteredArgs.slice(1));
1✔
72
        break;
1✔
73
      case 'status':
74
        await runStatus(migrator);
1✔
75
        break;
1✔
76
      case 'generate':
77
      case 'create':
78
        await runGenerate(migrator, filteredArgs.slice(1));
2✔
79
        break;
2✔
80
      case 'generate:entities':
81
      case 'generate-entities':
82
        await runGenerateFromEntities(migrator, filteredArgs.slice(1));
2✔
83
        break;
2✔
84
      case 'generate:from-db':
85
      case 'generate-from-db':
86
        await runGenerateFromDb(migrator, filteredArgs.slice(1), config);
×
87
        break;
×
88
      case 'sync':
89
        await runSync(migrator, filteredArgs.slice(1), config);
2✔
90
        break;
2✔
91
      case 'pending':
92
        await runPending(migrator);
1✔
93
        break;
1✔
94
      case 'drift:check':
95
      case 'drift-check':
96
        await runDriftCheck(migrator, config);
1✔
97
        break;
1✔
98
      default:
99
        console.error(`Unknown command: ${command}`);
1✔
100
        printHelp();
1✔
101
        process.exit(1);
1✔
102
    }
103

104
    // Close the connection pool
105
    const pool = config.pool;
14✔
106
    if (pool.end) {
14!
107
      await pool.end();
14✔
108
    }
109
  } catch (error) {
110
    console.error('Error:', (error as Error).message);
3✔
111
    process.exit(1);
3✔
112
  }
113
}
114

115
export async function runUp(migrator: Migrator, args: string[]) {
116
  const options: { to?: string; step?: number } = {};
8✔
117

118
  for (let i = 0; i < args.length; i++) {
8✔
119
    if (args[i] === '--to' && args[i + 1]) {
2✔
120
      options.to = args[++i];
1✔
121
    } else if (args[i] === '--step' && args[i + 1]) {
1!
122
      options.step = Number.parseInt(args[++i], 10);
1✔
123
    }
124
  }
125

126
  const results = await migrator.up(options);
8✔
127

128
  if (results.length === 0) {
6✔
129
    console.log('No pending migrations.');
4✔
130
    return;
4✔
131
  }
132

133
  const successful = results.filter((r) => r.success).length;
2✔
134
  const failed = results.filter((r) => !r.success).length;
2✔
135

136
  console.log(`\nMigrations complete: ${successful} successful, ${failed} failed`);
2✔
137

138
  if (failed > 0) {
2✔
139
    process.exit(1);
1✔
140
  }
141
}
142

143
export async function runDown(migrator: Migrator, args: string[]) {
144
  const options: { to?: string; step?: number } = { step: 1 }; // Default to 1 step
4✔
145

146
  for (let i = 0; i < args.length; i++) {
4✔
147
    if (args[i] === '--to' && args[i + 1]) {
3✔
148
      options.to = args[++i];
1✔
149
      delete options.step;
1✔
150
    } else if (args[i] === '--step' && args[i + 1]) {
2✔
151
      options.step = Number.parseInt(args[++i], 10);
1✔
152
    } else if (args[i] === '--all') {
1!
153
      delete options.step;
1✔
154
    }
155
  }
156

157
  const results = await migrator.down(options);
4✔
158

159
  if (results.length === 0) {
4✔
160
    console.log('No migrations to rollback.');
2✔
161
    return;
2✔
162
  }
163

164
  const successful = results.filter((r) => r.success).length;
2✔
165
  const failed = results.filter((r) => !r.success).length;
2✔
166

167
  console.log(`\nRollback complete: ${successful} successful, ${failed} failed`);
2✔
168

169
  if (failed > 0) {
2✔
170
    process.exit(1);
1✔
171
  }
172
}
173

174
export async function runStatus(migrator: Migrator) {
175
  const status = await migrator.status();
3✔
176

177
  console.log('\n=== Migration Status ===\n');
3✔
178

179
  console.log('Executed migrations:');
3✔
180
  if (status.executed.length === 0) {
3✔
181
    console.log('  (none)');
2✔
182
  } else {
183
    for (const name of status.executed) {
1✔
184
      console.log(`  ✓ ${name}`);
1✔
185
    }
186
  }
187

188
  console.log('\nPending migrations:');
3✔
189
  if (status.pending.length === 0) {
3✔
190
    console.log('  (none)');
2✔
191
  } else {
192
    for (const name of status.pending) {
1✔
193
      console.log(`  ○ ${name}`);
1✔
194
    }
195
  }
196

197
  console.log('');
3✔
198
}
199

200
export async function runPending(migrator: Migrator) {
201
  const pending = await migrator.pending();
3✔
202

203
  if (pending.length === 0) {
3✔
204
    console.log('No pending migrations.');
2✔
205
    return;
2✔
206
  }
207

208
  console.log('Pending migrations:');
1✔
209
  for (const migration of pending) {
1✔
210
    console.log(`  ○ ${migration.name}`);
1✔
211
  }
212
}
213

214
export async function runGenerate(migrator: Migrator, args: string[]) {
215
  const name = args.join('_') || 'migration';
3!
216
  const filePath = await migrator.generate(name);
3✔
217
  console.log(`\nCreated migration: ${filePath}`);
3✔
218
}
219

220
export async function runGenerateFromEntities(migrator: Migrator, args: string[]) {
221
  const name = args.join('_') || 'schema';
3!
222
  const filePath = await migrator.generateFromEntities(name);
3✔
223
  console.log(`\nCreated migration from entities: ${filePath}`);
3✔
224
}
225

226
export async function runSync(migrator: Migrator, args: string[], config: Partial<Config>) {
227
  const force = args.includes('--force');
8✔
228
  const push = args.includes('--push');
8✔
229
  const pull = args.includes('--pull');
8✔
230
  const dryRun = args.includes('--dry-run');
8✔
231

232
  // Parse direction
233
  let direction: 'bidirectional' | 'entity-to-db' | 'db-to-entity' = 'bidirectional';
8✔
234
  for (let i = 0; i < args.length; i++) {
8✔
235
    if (args[i] === '--direction' && args[i + 1]) {
6✔
236
      direction = args[++i] as typeof direction;
1✔
237
    }
238
  }
239

240
  // Shorthand flags
241
  if (push) direction = 'entity-to-db';
8✔
242
  if (pull) direction = 'db-to-entity';
8✔
243

244
  if (force) {
8✔
245
    console.log('\n⚠️  WARNING: This will drop and recreate all tables!');
2✔
246
    console.log('   All data will be lost. This should only be used in development.\n');
2✔
247
    await migrator.sync({ force });
2✔
248
    console.log('\nSchema sync completed.');
2✔
249
    return;
2✔
250
  }
251

252
  // Use SchemaSync for direction-aware sync
253
  if (config.entities && migrator.schemaIntrospector) {
6✔
254
    const schemaSync = createSchemaSync({
4✔
255
      entities: config.entities,
256
      introspector: migrator.schemaIntrospector,
257
      direction,
258
      safe: !args.includes('--unsafe'),
259
      dryRun,
260
    });
261

262
    const result = await schemaSync.sync();
4✔
263

264
    console.log('\n' + result.summary);
4✔
265

266
    if (result.conflicts.length > 0) {
4!
267
      console.log('\n⚠️  Conflicts require manual resolution.');
×
268
      process.exit(1);
×
269
    }
270
  } else {
271
    await migrator.sync({ force: false });
2✔
272
    console.log('\nSchema sync completed.');
2✔
273
  }
274
}
275

276
export async function runGenerateFromDb(migrator: Migrator, args: string[], config: Partial<Config>) {
277
  // Parse output directory
278
  let outputDir = './src/entities';
1✔
279
  for (let i = 0; i < args.length; i++) {
1✔
280
    if ((args[i] === '--output' || args[i] === '-o') && args[i + 1]) {
×
281
      outputDir = args[++i];
×
282
    }
283
  }
284

285
  if (!migrator.schemaIntrospector) {
1!
286
    console.error('No introspector available. Check your pool configuration.');
1✔
287
    process.exit(1);
1✔
288
  } else {
289
    console.log('\nAnalyzing database schema...');
×
290

291
    const ast = await migrator.schemaIntrospector.introspect();
×
292
    const tableCount = ast.tables.size;
×
293

294
    console.log(`Found ${tableCount} table(s): ${Array.from(ast.tables.keys()).join(', ')}`);
×
295
    console.log('\nGenerating entities...');
×
296

297
    const generator = createEntityCodeGenerator(ast, {
×
298
      addSyncComments: true,
299
      includeRelations: true,
300
      includeIndexes: true,
301
    });
302

303
    const entities = generator.generateAll();
×
304

305
    // Ensure output directory exists
306
    if (!fs.existsSync(outputDir)) {
×
307
      fs.mkdirSync(outputDir, { recursive: true });
×
308
    }
309

310
    // Write entity files
311
    for (const entity of entities) {
×
312
      const filePath = path.join(outputDir, entity.fileName);
×
313
      fs.writeFileSync(filePath, entity.code, 'utf-8');
×
314
      console.log(`  ✓ ${entity.className} -> ${filePath}`);
×
315
    }
316

317
    console.log(`\nGenerated ${entities.length} entities to ${outputDir}`);
×
318
  }
319
}
320

321
export async function runDriftCheck(migrator: Migrator, config: Partial<Config>) {
322
  if (!config.entities || config.entities.length === 0) {
3✔
323
    console.error('No entities configured. Add entities to your uql config.');
1✔
324
    process.exit(1);
1✔
325
  } else if (!migrator.schemaIntrospector) {
2!
326
    console.error('No introspector available. Check your pool configuration.');
×
327
    process.exit(1);
×
328
  } else {
329
    console.log('\nChecking for schema drift...');
2✔
330

331
    // Build expected schema from entities
332
    const builder = new SchemaASTBuilder();
2✔
333
    const expectedAST = builder.fromEntities(config.entities);
2✔
334

335
    // Build actual schema from database
336
    const actualAST = await migrator.schemaIntrospector.introspect();
2✔
337

338
    // Detect drift
339
    const report = detectDrift(expectedAST, actualAST);
2✔
340

341
    printDriftReport(report);
2✔
342
  }
343
}
344

345
function printDriftReport(report: DriftReport) {
346
  console.log('\n=== Schema Drift Report ===\n');
2✔
347

348
  if (report.status === 'in_sync') {
2✔
349
    console.log('✓ Schema is in sync.');
1✔
350
  } else {
351
    const statusIcon = report.status === 'critical' ? '✗' : '⚠️';
1!
352
    console.log(
1✔
353
      `${statusIcon} Status: ${report.status.toUpperCase()} (${report.summary.critical} critical, ${report.summary.warning} warning, ${report.summary.info} info)\n`,
354
    );
355

356
    // Group by severity
357
    const critical = report.drifts.filter((d) => d.severity === 'critical');
1✔
358
    const warning = report.drifts.filter((d) => d.severity === 'warning');
1✔
359
    const info = report.drifts.filter((d) => d.severity === 'info');
1✔
360

361
    printDriftGroup('CRITICAL:', critical, '✗', true);
1✔
362
    printDriftGroup('WARNINGS:', warning, '⚠', true);
1✔
363
    printDriftGroup('INFO:', info, 'ℹ', false);
1✔
364

365
    if (report.status === 'critical') {
1!
366
      process.exit(1);
1✔
367
    }
368
  }
369
}
370

371
function printDriftGroup(title: string, drifts: Drift[], icon: string, showSuggestion: boolean) {
372
  if (drifts.length > 0) {
3✔
373
    console.log(title);
1✔
374
    for (const drift of drifts) {
1✔
375
      console.log(`  ${icon} ${drift.table}${drift.column ? '.' + drift.column : ''} - ${drift.type}`);
1!
376
      console.log(`    ${drift.details}`);
1✔
377
      if (drift.expected && drift.actual) {
1!
378
        console.log(`    Expected: ${drift.expected}, Actual: ${drift.actual}`);
×
379
      }
380
      if (showSuggestion && drift.suggestion) {
1!
381
        console.log(`    → ${drift.suggestion}`);
1✔
382
      }
383
    }
384
    console.log('');
1✔
385
  }
386
}
387

388
export function printHelp() {
389
  console.log(`
4✔
390
uql-orm/migrate - Database migration tool for uql ORM
391

392
Usage: uql-orm/migrate <command> [options]
393

394
Commands:
395
  up                    Run all pending migrations
396
    --to <name>         Run migrations up to and including <name>
397
    --step <n>          Run only <n> migrations
398

399
  down                  Rollback the last migration
400
    --to <name>         Rollback to (and including) migration <name>
401
    --step <n>          Rollback <n> migrations (default: 1)
402
    --all               Rollback all migrations
403

404
  status                Show migration status
405

406
  pending               Show pending migrations
407

408
  generate <name>       Create a new empty migration file
409
  create <name>         Alias for generate
410

411
  generate:entities     Generate migration from entity definitions
412
    <name>              Optional name for the migration
413

414
  generate:from-db      Generate TypeScript entities from database
415
    --output, -o <dir>  Output directory (default: ./src/entities)
416

417
  sync                  Sync schema with direction support
418
    --force             Drop and recreate all tables (dangerous!)
419
    --direction <mode>  Sync direction: bidirectional, entity-to-db, db-to-entity
420
    --push              Shorthand for --direction entity-to-db
421
    --pull              Shorthand for --direction db-to-entity
422
    --dry-run           Preview changes without applying
423
    --unsafe            Allow destructive changes
424

425
  drift:check           Check for schema drift between entities and database
426

427
Configuration:
428
  Create a uql.config.ts or uql.config.js file in your project root.
429
  You can also specify a custom config path using --config or -c.
430
  The CLI requires pool.dialect (dialect id = pool.dialect.dialectName).
431
  See the repo README section "Driver → pool → dialect class".
432

433
  export default {
434
    pool: new PgQuerierPool({ ... }),
435
    migrationsPath: './migrations',
436
    tableName: 'uql_migrations',
437
    entities: [User, Post, ...],
438
  };
439

440
Examples:
441
  uql-orm/migrate up
442
  uql-orm/migrate up --step 1
443
  uql-orm/migrate down
444
  uql-orm/migrate down --step 3
445
  uql-orm/migrate status
446
  uql-orm/migrate generate add_users_table
447
  uql-orm/migrate generate:entities initial_schema
448
  uql-orm/migrate generate:from-db --output ./src/entities
449
  uql-orm/migrate sync --push
450
  uql-orm/migrate sync --pull
451
  uql-orm/migrate drift:check
452
`);
453
}
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