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

rogerpadilla / uql / 23874861559

01 Apr 2026 10:56PM UTC coverage: 94.915% (-0.03%) from 94.943%
23874861559

push

github

rogerpadilla
refactor: streamline Bun SQL Postgres update calls in tests for improved readability

2976 of 3299 branches covered (90.21%)

Branch coverage included in aggregate %.

5256 of 5374 relevant lines covered (97.8%)

346.24 hits per line

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

83.99
/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 { createSchemaSync } from './sync/schemaSync.js';
15

16
export function getSchemaGenerator(dialect: AbstractDialect, namingStrategy?: NamingStrategy) {
17
  return createSchemaGenerator(dialect, namingStrategy);
22✔
18
}
19

20
export async function main(args = process.argv.slice(2)) {
20✔
21
  let customPath: string | undefined;
22
  const filteredArgs: string[] = [];
20✔
23

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

32
  const command = filteredArgs[0];
20✔
33

34
  if (!command || command === '--help' || command === '-h') {
20✔
35
    printHelp();
3✔
36
    return;
3✔
37
  }
38

39
  try {
17✔
40
    const config = await loadConfig(customPath);
17✔
41
    assertCliConfig(config);
17✔
42

43
    const dialectName = config.pool.dialect.dialectName ?? 'postgres';
17!
44

45
    const options: MigratorOptions = {
20✔
46
      migrationsPath: config.migrationsPath ?? './migrations',
36✔
47
      tableName: config.tableName,
48
      logger: console.log,
49
      entities: config.entities,
50
      namingStrategy: config.namingStrategy,
51
    };
52

53
    const migrator = new Migrator(config.pool, options);
20✔
54
    const generator = getSchemaGenerator(config.pool.dialect, config.namingStrategy);
20✔
55
    if (!generator) {
20!
56
      throw new TypeError(`Could not find a schema generator for dialect: ${dialectName}`);
×
57
    }
58
    migrator.setSchemaGenerator(generator);
16✔
59

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

98
    // Close the connection pool
99
    const pool = config.pool;
14✔
100
    if (pool.end) {
14!
101
      await pool.end();
14✔
102
    }
103
  } catch (error) {
104
    console.error('Error:', (error as Error).message);
3✔
105
    process.exit(1);
3✔
106
  }
107
}
108

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

112
  for (let i = 0; i < args.length; i++) {
8✔
113
    if (args[i] === '--to' && args[i + 1]) {
2✔
114
      options.to = args[++i];
1✔
115
    } else if (args[i] === '--step' && args[i + 1]) {
1!
116
      options.step = Number.parseInt(args[++i], 10);
1✔
117
    }
118
  }
119

120
  const results = await migrator.up(options);
8✔
121

122
  if (results.length === 0) {
6✔
123
    console.log('No pending migrations.');
4✔
124
    return;
4✔
125
  }
126

127
  const successful = results.filter((r) => r.success).length;
2✔
128
  const failed = results.filter((r) => !r.success).length;
2✔
129

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

132
  if (failed > 0) {
2✔
133
    process.exit(1);
1✔
134
  }
135
}
136

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

140
  for (let i = 0; i < args.length; i++) {
4✔
141
    if (args[i] === '--to' && args[i + 1]) {
3✔
142
      options.to = args[++i];
1✔
143
      delete options.step;
1✔
144
    } else if (args[i] === '--step' && args[i + 1]) {
2✔
145
      options.step = Number.parseInt(args[++i], 10);
1✔
146
    } else if (args[i] === '--all') {
1!
147
      delete options.step;
1✔
148
    }
149
  }
150

151
  const results = await migrator.down(options);
4✔
152

153
  if (results.length === 0) {
4✔
154
    console.log('No migrations to rollback.');
2✔
155
    return;
2✔
156
  }
157

158
  const successful = results.filter((r) => r.success).length;
2✔
159
  const failed = results.filter((r) => !r.success).length;
2✔
160

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

163
  if (failed > 0) {
2✔
164
    process.exit(1);
1✔
165
  }
166
}
167

168
export async function runStatus(migrator: Migrator) {
169
  const status = await migrator.status();
3✔
170

171
  console.log('\n=== Migration Status ===\n');
3✔
172

173
  console.log('Executed migrations:');
3✔
174
  if (status.executed.length === 0) {
3✔
175
    console.log('  (none)');
2✔
176
  } else {
177
    for (const name of status.executed) {
1✔
178
      console.log(`  ✓ ${name}`);
1✔
179
    }
180
  }
181

182
  console.log('\nPending migrations:');
3✔
183
  if (status.pending.length === 0) {
3✔
184
    console.log('  (none)');
2✔
185
  } else {
186
    for (const name of status.pending) {
1✔
187
      console.log(`  ○ ${name}`);
1✔
188
    }
189
  }
190

191
  console.log('');
3✔
192
}
193

194
export async function runPending(migrator: Migrator) {
195
  const pending = await migrator.pending();
3✔
196

197
  if (pending.length === 0) {
3✔
198
    console.log('No pending migrations.');
2✔
199
    return;
2✔
200
  }
201

202
  console.log('Pending migrations:');
1✔
203
  for (const migration of pending) {
1✔
204
    console.log(`  ○ ${migration.name}`);
1✔
205
  }
206
}
207

208
export async function runGenerate(migrator: Migrator, args: string[]) {
209
  const name = args.join('_') || 'migration';
3!
210
  const filePath = await migrator.generate(name);
3✔
211
  console.log(`\nCreated migration: ${filePath}`);
3✔
212
}
213

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

220
export async function runSync(migrator: Migrator, args: string[], config: Partial<Config>) {
221
  const force = args.includes('--force');
8✔
222
  const push = args.includes('--push');
8✔
223
  const pull = args.includes('--pull');
8✔
224
  const dryRun = args.includes('--dry-run');
8✔
225

226
  // Parse direction
227
  let direction: 'bidirectional' | 'entity-to-db' | 'db-to-entity' = 'bidirectional';
8✔
228
  for (let i = 0; i < args.length; i++) {
8✔
229
    if (args[i] === '--direction' && args[i + 1]) {
6✔
230
      direction = args[++i] as typeof direction;
1✔
231
    }
232
  }
233

234
  // Shorthand flags
235
  if (push) direction = 'entity-to-db';
8✔
236
  if (pull) direction = 'db-to-entity';
8✔
237

238
  if (force) {
8✔
239
    console.log('\n⚠️  WARNING: This will drop and recreate all tables!');
2✔
240
    console.log('   All data will be lost. This should only be used in development.\n');
2✔
241
    await migrator.sync({ force });
2✔
242
    console.log('\nSchema sync completed.');
2✔
243
    return;
2✔
244
  }
245

246
  // Use SchemaSync for direction-aware sync
247
  if (config.entities && migrator.schemaIntrospector) {
6✔
248
    const schemaSync = createSchemaSync({
4✔
249
      entities: config.entities,
250
      introspector: migrator.schemaIntrospector,
251
      direction,
252
      safe: !args.includes('--unsafe'),
253
      dryRun,
254
    });
255

256
    const result = await schemaSync.sync();
4✔
257

258
    console.log('\n' + result.summary);
4✔
259

260
    if (result.conflicts.length > 0) {
4!
261
      console.log('\n⚠️  Conflicts require manual resolution.');
×
262
      process.exit(1);
×
263
    }
264
  } else {
265
    await migrator.sync({ force: false });
2✔
266
    console.log('\nSchema sync completed.');
2✔
267
  }
268
}
269

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

279
  if (!migrator.schemaIntrospector) {
1!
280
    console.error('No introspector available. Check your pool configuration.');
1✔
281
    process.exit(1);
1✔
282
  } else {
283
    console.log('\nAnalyzing database schema...');
×
284

285
    const ast = await migrator.schemaIntrospector.introspect();
×
286
    const tableCount = ast.tables.size;
×
287

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

291
    const generator = createEntityCodeGenerator(ast, {
×
292
      addSyncComments: true,
293
      includeRelations: true,
294
      includeIndexes: true,
295
    });
296

297
    const entities = generator.generateAll();
×
298

299
    // Ensure output directory exists
300
    if (!fs.existsSync(outputDir)) {
×
301
      fs.mkdirSync(outputDir, { recursive: true });
×
302
    }
303

304
    // Write entity files
305
    for (const entity of entities) {
×
306
      const filePath = path.join(outputDir, entity.fileName);
×
307
      fs.writeFileSync(filePath, entity.code, 'utf-8');
×
308
      console.log(`  ✓ ${entity.className} -> ${filePath}`);
×
309
    }
310

311
    console.log(`\nGenerated ${entities.length} entities to ${outputDir}`);
×
312
  }
313
}
314

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

325
    // Build expected schema from entities
326
    const builder = new SchemaASTBuilder();
2✔
327
    const expectedAST = builder.fromEntities(config.entities);
2✔
328

329
    // Build actual schema from database
330
    const actualAST = await migrator.schemaIntrospector.introspect();
2✔
331

332
    // Detect drift
333
    const report = detectDrift(expectedAST, actualAST);
2✔
334

335
    printDriftReport(report);
2✔
336
  }
337
}
338

339
function printDriftReport(report: DriftReport) {
340
  console.log('\n=== Schema Drift Report ===\n');
2✔
341

342
  if (report.status === 'in_sync') {
2✔
343
    console.log('✓ Schema is in sync.');
1✔
344
  } else {
345
    const statusIcon = report.status === 'critical' ? '✗' : '⚠️';
1!
346
    console.log(
1✔
347
      `${statusIcon} Status: ${report.status.toUpperCase()} (${report.summary.critical} critical, ${report.summary.warning} warning, ${report.summary.info} info)\n`,
348
    );
349

350
    // Group by severity
351
    const critical = report.drifts.filter((d) => d.severity === 'critical');
1✔
352
    const warning = report.drifts.filter((d) => d.severity === 'warning');
1✔
353
    const info = report.drifts.filter((d) => d.severity === 'info');
1✔
354

355
    printDriftGroup('CRITICAL:', critical, '✗', true);
1✔
356
    printDriftGroup('WARNINGS:', warning, '⚠', true);
1✔
357
    printDriftGroup('INFO:', info, 'ℹ', false);
1✔
358

359
    if (report.status === 'critical') {
1!
360
      process.exit(1);
1✔
361
    }
362
  }
363
}
364

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

382
export function printHelp() {
383
  console.log(`
4✔
384
uql-orm/migrate - Database migration tool for uql ORM
385

386
Usage: uql-orm/migrate <command> [options]
387

388
Commands:
389
  up                    Run all pending migrations
390
    --to <name>         Run migrations up to and including <name>
391
    --step <n>          Run only <n> migrations
392

393
  down                  Rollback the last migration
394
    --to <name>         Rollback to (and including) migration <name>
395
    --step <n>          Rollback <n> migrations (default: 1)
396
    --all               Rollback all migrations
397

398
  status                Show migration status
399

400
  pending               Show pending migrations
401

402
  generate <name>       Create a new empty migration file
403
  create <name>         Alias for generate
404

405
  generate:entities     Generate migration from entity definitions
406
    <name>              Optional name for the migration
407

408
  generate:from-db      Generate TypeScript entities from database
409
    --output, -o <dir>  Output directory (default: ./src/entities)
410

411
  sync                  Sync schema with direction support
412
    --force             Drop and recreate all tables (dangerous!)
413
    --direction <mode>  Sync direction: bidirectional, entity-to-db, db-to-entity
414
    --push              Shorthand for --direction entity-to-db
415
    --pull              Shorthand for --direction db-to-entity
416
    --dry-run           Preview changes without applying
417
    --unsafe            Allow destructive changes
418

419
  drift:check           Check for schema drift between entities and database
420

421
Configuration:
422
  Create a uql.config.ts or uql.config.js file in your project root.
423
  You can also specify a custom config path using --config or -c.
424
  The CLI requires pool.dialect (dialect id = pool.dialect.dialectName).
425
  See the repo README section "Driver → pool → dialect class".
426

427
  export default {
428
    pool: new PgQuerierPool({ ... }),
429
    migrationsPath: './migrations',
430
    tableName: 'uql_migrations',
431
    entities: [User, Post, ...],
432
  };
433

434
Examples:
435
  uql-orm/migrate up
436
  uql-orm/migrate up --step 1
437
  uql-orm/migrate down
438
  uql-orm/migrate down --step 3
439
  uql-orm/migrate status
440
  uql-orm/migrate generate add_users_table
441
  uql-orm/migrate generate:entities initial_schema
442
  uql-orm/migrate generate:from-db --output ./src/entities
443
  uql-orm/migrate sync --push
444
  uql-orm/migrate sync --pull
445
  uql-orm/migrate drift:check
446
`);
447
}
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