@dbsp/cli 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  createDbConnection,
4
4
  generateSchemaFileWithDiagnostics,
5
5
  redactDbUrl
6
- } from "./chunk-VUNVYYOZ.js";
6
+ } from "./chunk-G6NS4BAV.js";
7
7
  import {
8
8
  loadSchema,
9
9
  loadSchemaFromCwd
10
- } from "./chunk-T4ODDHZQ.js";
10
+ } from "./chunk-EO3GHEFW.js";
11
11
  import {
12
12
  config,
13
13
  validateIdentifier
@@ -52,16 +52,16 @@ var generateCommand = new Command("generate").description("Generate code from sc
52
52
  const log = useStdout ? console.error : console.log;
53
53
  log(`\u{1F4C4} Loaded schema from: ${schemaPath}`);
54
54
  const dialect = options.dialect ?? "postgresql";
55
- if (dialect !== "postgresql") {
55
+ if (!isDialectSupported(dialect)) {
56
56
  console.error(
57
57
  `\u26A0\uFE0F Warning: Only 'postgresql' dialect is currently supported. Using postgresql.`
58
58
  );
59
59
  }
60
60
  switch (target) {
61
61
  case "ddl": {
62
- const casing = options.casing ?? "snake";
62
+ const dbCasing = mapCasingToDbCasing(options.casing) ?? schema.dbCasing ?? "snake_case";
63
+ const casingLabel = formatGenerateCasingLabel(dbCasing);
63
64
  const { createPgsqlCompileOnlyAdapter } = await import("@dbsp/adapter-pgsql");
64
- const dbCasing = casing === "snake" ? "snake_case" : "preserve";
65
65
  const adapter = createPgsqlCompileOnlyAdapter({
66
66
  dbCasing,
67
67
  ...options.schemaName ? { schemaName: options.schemaName } : {}
@@ -81,7 +81,7 @@ var generateCommand = new Command("generate").description("Generate code from sc
81
81
  console.log(`\u2705 Generated DDL: ${outPath}`);
82
82
  console.log(` Tables: ${schema.tableNames.length}`);
83
83
  console.log(` Statements: ${ddlStatements.length}`);
84
- console.log(` Casing: ${casing}`);
84
+ console.log(` Casing: ${casingLabel}`);
85
85
  if (options.drop) {
86
86
  console.log(` Includes DROP statements`);
87
87
  }
@@ -102,6 +102,31 @@ var generateCommand = new Command("generate").description("Generate code from sc
102
102
  }
103
103
  }
104
104
  );
105
+ function isDialectSupported(dialect) {
106
+ return dialect === "postgresql";
107
+ }
108
+ function mapCasingToDbCasing(casing) {
109
+ switch (casing) {
110
+ case "snake":
111
+ return "snake_case";
112
+ case "camel":
113
+ return "camelCase";
114
+ case "none":
115
+ return "preserve";
116
+ default:
117
+ return void 0;
118
+ }
119
+ }
120
+ function formatGenerateCasingLabel(dbCasing) {
121
+ switch (dbCasing) {
122
+ case "snake_case":
123
+ return "snake";
124
+ case "camelCase":
125
+ return "camel";
126
+ case "preserve":
127
+ return "none";
128
+ }
129
+ }
105
130
 
106
131
  // src/commands/introspect.ts
107
132
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -142,10 +167,10 @@ var introspectCommand = new Command2("introspect").description("Generate schema.
142
167
  try {
143
168
  const { pool } = await createDbConnection(options.db);
144
169
  try {
145
- const { introspect: introspect4 } = await import("@dbsp/adapter-pgsql");
170
+ const { introspect } = await import("@dbsp/adapter-pgsql");
146
171
  const excludePatterns = options.exclude ? options.exclude.split(",").map((s) => s.trim()) : void 0;
147
172
  const includePatterns = options.include ? options.include.split(",").map((s) => s.trim()) : void 0;
148
- const model = await introspect4(pool, {
173
+ const model = await introspect(pool, {
149
174
  schema: options.schemaName,
150
175
  ...excludePatterns ? { exclude: excludePatterns } : {},
151
176
  ...includePatterns ? { include: includePatterns } : {}
@@ -190,13 +215,13 @@ var introspectCommand = new Command2("introspect").description("Generate schema.
190
215
 
191
216
  // src/commands/migrate.ts
192
217
  import {
193
- compareSchemata,
218
+ comparePgsqlDatabaseSchema,
219
+ createPgsqlAdapter,
194
220
  ensureMigrationsTable,
195
221
  generateMigrationFile,
196
222
  generateMigrationSQL,
197
223
  getAppliedMigrations,
198
224
  getNextSchemaVersion,
199
- introspect,
200
225
  isDestructiveDown,
201
226
  parseMigrationFile,
202
227
  recordMigration,
@@ -321,6 +346,218 @@ function hasExecutableSql(stmt) {
321
346
  return t.length > 0 && !t.startsWith("--");
322
347
  });
323
348
  }
349
+ var defaultMigrateDeps = {
350
+ withMigratePool,
351
+ ensureMigrationsTable,
352
+ withMigrationLock,
353
+ getAppliedMigrations,
354
+ scanMigrationFiles,
355
+ parseMigrationFile,
356
+ getNextSchemaVersion,
357
+ recordMigration,
358
+ removeMigrationRecord,
359
+ isDestructiveDown
360
+ };
361
+ function resolveMigrateDeps(deps) {
362
+ return { ...defaultMigrateDeps, ...deps };
363
+ }
364
+ async function runApply(options, deps) {
365
+ const d = resolveMigrateDeps(deps);
366
+ console.log("\u{1F504} Applying migrations");
367
+ console.log(` Database: ${redactDbUrl(options.db)}`);
368
+ console.log(` Directory: ${options.dir}`);
369
+ console.log("");
370
+ await d.withMigratePool(options.db, async (pool) => {
371
+ await d.ensureMigrationsTable(pool);
372
+ await d.withMigrationLock(pool, async (client) => {
373
+ const applied = await d.getAppliedMigrations(client);
374
+ const appliedMap = new Map(applied.map((m) => [m.name, m.checksum]));
375
+ const files = d.scanMigrationFiles(options.dir);
376
+ if (files.length === 0) {
377
+ console.log(`No migration files found in ${options.dir}`);
378
+ return;
379
+ }
380
+ for (const file of files) {
381
+ const existingChecksum = appliedMap.get(file.name);
382
+ if (existingChecksum !== void 0 && existingChecksum !== file.checksum) {
383
+ throw new MigrationError(
384
+ `Checksum mismatch for ${file.name}
385
+ Expected: ${existingChecksum}
386
+ Got: ${file.checksum}
387
+
388
+ Migration file has been tampered with after being applied.`
389
+ );
390
+ }
391
+ }
392
+ const pending = files.filter((f) => !appliedMap.has(f.name));
393
+ if (pending.length === 0) {
394
+ console.log("\u2705 All migrations already applied.");
395
+ return;
396
+ }
397
+ if (options.dryRun) {
398
+ console.log(`${pending.length} pending migration(s):`);
399
+ for (const file of pending) {
400
+ console.log(` - ${file.name}`);
401
+ }
402
+ return;
403
+ }
404
+ let appliedCount = 0;
405
+ for (const file of pending) {
406
+ console.log(` Applying: ${file.name}...`);
407
+ const parsed = d.parseMigrationFile(file.content);
408
+ const statements = parsed.upStatements.filter(hasExecutableSql);
409
+ const version = await d.getNextSchemaVersion(client);
410
+ const destructive = parsed.destructive === true;
411
+ try {
412
+ await client.query("BEGIN");
413
+ for (const stmt of statements) {
414
+ await client.query(stmt);
415
+ }
416
+ await d.recordMigration(
417
+ client,
418
+ file.name,
419
+ file.checksum,
420
+ version,
421
+ destructive
422
+ );
423
+ await client.query("COMMIT");
424
+ } catch (applyError) {
425
+ let rollbackError;
426
+ try {
427
+ await client.query("ROLLBACK");
428
+ } catch (e) {
429
+ rollbackError = e;
430
+ }
431
+ const primary = sanitizePgError(applyError);
432
+ if (rollbackError !== void 0) {
433
+ console.error(
434
+ ` Note: rollback also failed: ${sanitizePgError(rollbackError).message}`
435
+ );
436
+ }
437
+ throw primary;
438
+ }
439
+ appliedCount++;
440
+ console.log(` \u2705 Applied: ${file.name}`);
441
+ }
442
+ console.log(`
443
+ \u2705 ${appliedCount} migration(s) applied successfully.`);
444
+ });
445
+ });
446
+ }
447
+ async function runRollback(options, deps) {
448
+ const d = resolveMigrateDeps(deps);
449
+ console.log(`\u23EA Rolling back ${options.count} migration(s)`);
450
+ console.log(` Database: ${redactDbUrl(options.db)}`);
451
+ console.log(` Directory: ${options.dir}`);
452
+ console.log("");
453
+ await d.withMigratePool(options.db, async (pool) => {
454
+ await d.ensureMigrationsTable(pool);
455
+ await d.withMigrationLock(pool, async (client) => {
456
+ const applied = await d.getAppliedMigrations(client);
457
+ const sortedDesc = [...applied].sort(
458
+ (a, b) => b.name.localeCompare(a.name)
459
+ );
460
+ if (options.count > sortedDesc.length) {
461
+ throw new MigrationError(
462
+ `Cannot roll back ${options.count} migration(s) \u2014 only ${sortedDesc.length} applied`
463
+ );
464
+ }
465
+ const toRollback = sortedDesc.slice(0, options.count);
466
+ const files = d.scanMigrationFiles(options.dir);
467
+ const fileMap = new Map(files.map((f) => [f.name, f]));
468
+ let rolledBack = 0;
469
+ for (const record of toRollback) {
470
+ const file = fileMap.get(record.name);
471
+ if (!file) {
472
+ throw new MigrationError(
473
+ `Migration file not found on disk: ${record.name}`
474
+ );
475
+ }
476
+ if (file.checksum !== record.checksum) {
477
+ throw new MigrationError(
478
+ `Checksum mismatch for ${record.name}
479
+ Expected: ${record.checksum}
480
+ Got: ${file.checksum}
481
+
482
+ Migration file has been modified since it was applied.`
483
+ );
484
+ }
485
+ const parsed = d.parseMigrationFile(file.content);
486
+ if (!parsed.hasDown) {
487
+ throw new MigrationError(
488
+ `Migration ${record.name} has no DOWN section
489
+ Cannot roll back a migration without a DOWN section.`
490
+ );
491
+ }
492
+ const downStmts = parsed.downStatements.filter(hasExecutableSql);
493
+ if (downStmts.length === 0 && !options.force) {
494
+ throw new MigrationError(
495
+ `Migration ${record.name} has an empty DOWN section
496
+ Use --force to roll back anyway.`
497
+ );
498
+ }
499
+ const safeHeaderContradictedByDown = parsed.destructive === false && d.isDestructiveDown(downStmts);
500
+ if ((parsed.destructive !== false || safeHeaderContradictedByDown) && !options.force) {
501
+ if (parsed.destructive === true) {
502
+ throw new MigrationError(
503
+ `Migration ${record.name} has destructive DOWN operations
504
+ Use --force to proceed with destructive rollback.`
505
+ );
506
+ }
507
+ if (safeHeaderContradictedByDown) {
508
+ throw new MigrationError(
509
+ `Migration ${record.name} is marked non-destructive but its DOWN section contains an obvious destructive statement
510
+ Re-generate the migration with accurate dbsp metadata or pass --force.`
511
+ );
512
+ }
513
+ throw new MigrationError(
514
+ `Migration ${record.name} is unmarked or legacy
515
+ Re-generate the migration with dbsp metadata or pass --force.`
516
+ );
517
+ }
518
+ if (downStmts.length > 0 || options.force) {
519
+ console.log(` Rolling back: ${record.name}...`);
520
+ }
521
+ try {
522
+ await client.query("BEGIN");
523
+ for (const stmt of downStmts) {
524
+ await client.query(stmt);
525
+ }
526
+ await d.removeMigrationRecord(client, record.name);
527
+ await client.query("COMMIT");
528
+ } catch (rollbackError) {
529
+ let rollbackCleanupError;
530
+ try {
531
+ await client.query("ROLLBACK");
532
+ } catch (e) {
533
+ rollbackCleanupError = e;
534
+ }
535
+ const primary = sanitizePgError(rollbackError);
536
+ if (rollbackCleanupError !== void 0) {
537
+ console.error(
538
+ ` Note: rollback also failed: ${sanitizePgError(rollbackCleanupError).message}`
539
+ );
540
+ }
541
+ throw primary;
542
+ }
543
+ rolledBack++;
544
+ console.log(` \u2705 Rolled back: ${record.name}`);
545
+ }
546
+ console.log(`
547
+ \u2705 ${rolledBack} migration(s) rolled back successfully.`);
548
+ });
549
+ });
550
+ }
551
+ function runRollbackCommand(countArg, options) {
552
+ const count = Number.parseInt(countArg, 10);
553
+ if (Number.isNaN(count) || count < 1) {
554
+ console.error("\u274C Count must be a positive integer");
555
+ process.exit(1);
556
+ }
557
+ return runMigrateAction(async () => {
558
+ await runRollback({ count, ...options });
559
+ });
560
+ }
324
561
  var devCommand = new Command3("dev").description("Generate a migration from schema changes").option(
325
562
  "-s, --schema <path>",
326
563
  "Path to schema file (default: dbsp.schema.ts)"
@@ -333,10 +570,12 @@ var devCommand = new Command3("dev").description("Generate a migration from sche
333
570
  const loaded = await loadSchema(schemaPath);
334
571
  const schemaModel = loaded.model;
335
572
  await withMigratePool(options.db, async (pool) => {
336
- const dbModel = await introspect(pool, {
337
- ...options.schemaName ? { schema: options.schemaName } : {}
573
+ const adapter = createPgsqlAdapter(pool);
574
+ const diff = await comparePgsqlDatabaseSchema(adapter, schemaModel, {
575
+ ...options.schemaName ? { schema: options.schemaName } : {},
576
+ ...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {},
577
+ onWarning: (message) => console.warn(`\u26A0\uFE0F ${message}`)
338
578
  });
339
- const diff = compareSchemata(schemaModel, dbModel);
340
579
  if (diff.changes.length === 0) {
341
580
  console.log("\u2705 No changes detected \u2014 database matches schema.");
342
581
  return;
@@ -379,211 +618,10 @@ var devCommand = new Command3("dev").description("Generate a migration from sche
379
618
  );
380
619
  var applyCommand = new Command3("apply").description("Apply pending migrations").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--dry-run", "Show pending migrations without applying").action(
381
620
  (options) => runMigrateAction(async () => {
382
- console.log("\u{1F504} Applying migrations");
383
- console.log(` Database: ${redactDbUrl(options.db)}`);
384
- console.log(` Directory: ${options.dir}`);
385
- console.log("");
386
- await withMigratePool(options.db, async (pool) => {
387
- await ensureMigrationsTable(pool);
388
- await withMigrationLock(pool, async (client) => {
389
- const applied = await getAppliedMigrations(client);
390
- const appliedMap = new Map(applied.map((m) => [m.name, m.checksum]));
391
- const files = scanMigrationFiles(options.dir);
392
- if (files.length === 0) {
393
- console.log(`No migration files found in ${options.dir}`);
394
- return;
395
- }
396
- for (const file of files) {
397
- const existingChecksum = appliedMap.get(file.name);
398
- if (existingChecksum !== void 0 && existingChecksum !== file.checksum) {
399
- throw new MigrationError(
400
- `Checksum mismatch for ${file.name}
401
- Expected: ${existingChecksum}
402
- Got: ${file.checksum}
403
-
404
- Migration file has been tampered with after being applied.`
405
- );
406
- }
407
- }
408
- const pending = files.filter((f) => !appliedMap.has(f.name));
409
- if (pending.length === 0) {
410
- console.log("\u2705 All migrations already applied.");
411
- return;
412
- }
413
- if (options.dryRun) {
414
- console.log(`${pending.length} pending migration(s):`);
415
- for (const file of pending) {
416
- console.log(` - ${file.name}`);
417
- }
418
- return;
419
- }
420
- let appliedCount = 0;
421
- for (const file of pending) {
422
- console.log(` Applying: ${file.name}...`);
423
- const parsed = parseMigrationFile(file.content);
424
- const statements = parsed.upStatements.filter(hasExecutableSql);
425
- const version = await getNextSchemaVersion(
426
- client
427
- );
428
- const destructive = parsed.destructive === true;
429
- try {
430
- await client.query("BEGIN");
431
- for (const stmt of statements) {
432
- await client.query(stmt);
433
- }
434
- await recordMigration(
435
- client,
436
- file.name,
437
- file.checksum,
438
- version,
439
- destructive
440
- );
441
- await client.query("COMMIT");
442
- } catch (applyError) {
443
- let rollbackError;
444
- try {
445
- await client.query("ROLLBACK");
446
- } catch (e) {
447
- rollbackError = e;
448
- }
449
- const primary = sanitizePgError(applyError);
450
- if (rollbackError !== void 0) {
451
- console.error(
452
- ` Note: rollback also failed: ${sanitizePgError(rollbackError).message}`
453
- );
454
- }
455
- throw primary;
456
- }
457
- appliedCount++;
458
- console.log(` \u2705 Applied: ${file.name}`);
459
- }
460
- console.log(
461
- `
462
- \u2705 ${appliedCount} migration(s) applied successfully.`
463
- );
464
- });
465
- });
621
+ await runApply(options);
466
622
  })
467
623
  );
468
- var rollbackCommand = new Command3("rollback").description("Roll back applied migrations").argument("<count>", "Number of migrations to roll back").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--force", "Force rollback of destructive or empty DOWN migrations").action(
469
- (countArg, options) => {
470
- const count = Number.parseInt(countArg, 10);
471
- if (Number.isNaN(count) || count < 1) {
472
- console.error("\u274C Count must be a positive integer");
473
- process.exit(1);
474
- }
475
- return runMigrateAction(async () => {
476
- console.log(`\u23EA Rolling back ${count} migration(s)`);
477
- console.log(` Database: ${redactDbUrl(options.db)}`);
478
- console.log(` Directory: ${options.dir}`);
479
- console.log("");
480
- await withMigratePool(options.db, async (pool) => {
481
- await ensureMigrationsTable(pool);
482
- await withMigrationLock(pool, async (client) => {
483
- const applied = await getAppliedMigrations(
484
- client
485
- );
486
- const sortedDesc = [...applied].sort(
487
- (a, b) => b.name.localeCompare(a.name)
488
- );
489
- if (count > sortedDesc.length) {
490
- throw new MigrationError(
491
- `Cannot roll back ${count} migration(s) \u2014 only ${sortedDesc.length} applied`
492
- );
493
- }
494
- const toRollback = sortedDesc.slice(0, count);
495
- const files = scanMigrationFiles(options.dir);
496
- const fileMap = new Map(files.map((f) => [f.name, f]));
497
- let rolledBack = 0;
498
- for (const record of toRollback) {
499
- const file = fileMap.get(record.name);
500
- if (!file) {
501
- throw new MigrationError(
502
- `Migration file not found on disk: ${record.name}`
503
- );
504
- }
505
- if (file.checksum !== record.checksum) {
506
- throw new MigrationError(
507
- `Checksum mismatch for ${record.name}
508
- Expected: ${record.checksum}
509
- Got: ${file.checksum}
510
-
511
- Migration file has been modified since it was applied.`
512
- );
513
- }
514
- const parsed = parseMigrationFile(file.content);
515
- if (!parsed.hasDown) {
516
- throw new MigrationError(
517
- `Migration ${record.name} has no DOWN section
518
- Cannot roll back a migration without a DOWN section.`
519
- );
520
- }
521
- const downStmts = parsed.downStatements.filter(hasExecutableSql);
522
- if (downStmts.length === 0 && !options.force) {
523
- throw new MigrationError(
524
- `Migration ${record.name} has an empty DOWN section
525
- Use --force to roll back anyway.`
526
- );
527
- }
528
- const safeHeaderContradictedByDown = parsed.destructive === false && isDestructiveDown(downStmts);
529
- if ((parsed.destructive !== false || safeHeaderContradictedByDown) && !options.force) {
530
- if (parsed.destructive === true) {
531
- throw new MigrationError(
532
- `Migration ${record.name} has destructive DOWN operations
533
- Use --force to proceed with destructive rollback.`
534
- );
535
- }
536
- if (safeHeaderContradictedByDown) {
537
- throw new MigrationError(
538
- `Migration ${record.name} is marked non-destructive but its DOWN section contains an obvious destructive statement
539
- Re-generate the migration with accurate dbsp metadata or pass --force.`
540
- );
541
- }
542
- throw new MigrationError(
543
- `Migration ${record.name} is unmarked or legacy
544
- Re-generate the migration with dbsp metadata or pass --force.`
545
- );
546
- }
547
- if (downStmts.length > 0 || options.force) {
548
- console.log(` Rolling back: ${record.name}...`);
549
- }
550
- try {
551
- await client.query("BEGIN");
552
- for (const stmt of downStmts) {
553
- await client.query(stmt);
554
- }
555
- await removeMigrationRecord(
556
- client,
557
- record.name
558
- );
559
- await client.query("COMMIT");
560
- } catch (rollbackError) {
561
- let rollbackCleanupError;
562
- try {
563
- await client.query("ROLLBACK");
564
- } catch (e) {
565
- rollbackCleanupError = e;
566
- }
567
- const primary = sanitizePgError(rollbackError);
568
- if (rollbackCleanupError !== void 0) {
569
- console.error(
570
- ` Note: rollback also failed: ${sanitizePgError(rollbackCleanupError).message}`
571
- );
572
- }
573
- throw primary;
574
- }
575
- rolledBack++;
576
- console.log(` \u2705 Rolled back: ${record.name}`);
577
- }
578
- console.log(
579
- `
580
- \u2705 ${rolledBack} migration(s) rolled back successfully.`
581
- );
582
- });
583
- });
584
- });
585
- }
586
- );
624
+ var rollbackCommand = new Command3("rollback").description("Roll back applied migrations").argument("<count>", "Number of migrations to roll back").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--force", "Force rollback of destructive or empty DOWN migrations").action(runRollbackCommand);
587
625
  var statusCommand = new Command3("status").description("Show migration status").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--json", "Output as JSON").action(
588
626
  (options) => runMigrateAction(async () => {
589
627
  await withMigratePool(options.db, async (pool) => {
@@ -655,10 +693,11 @@ var migrateCommand = new Command3("migrate").description("Database migration man
655
693
 
656
694
  // src/commands/push.ts
657
695
  import {
658
- compareSchemata as compareSchemata2,
696
+ comparePgsqlDatabaseSchema as comparePgsqlDatabaseSchema2,
697
+ createPgsqlAdapter as createPgsqlAdapter2,
659
698
  generateDDL,
660
699
  generateMigrationSQL as generateMigrationSQL2,
661
- introspect as introspect2
700
+ getNamingPluginForDbCasing
662
701
  } from "@dbsp/adapter-pgsql";
663
702
  import { Command as Command4 } from "commander";
664
703
 
@@ -721,16 +760,10 @@ var pushCommand = new Command4("push").description("Push schema to database (add
721
760
  if (options.drop) {
722
761
  const statements = generateDDL(schemaModel, {
723
762
  includeDropStatements: true,
724
- ...options.schemaName ? { schemaName: options.schemaName } : {}
763
+ ...options.schemaName ? { schemaName: options.schemaName } : {},
764
+ ...loaded.dbCasing !== void 0 ? { naming: getNamingPluginForDbCasing(loaded.dbCasing) } : {}
725
765
  });
726
- const escapedTable = MIGRATIONS_TABLE.replace(
727
- /[.*+?^${}()|[\]\\]/g,
728
- "\\$&"
729
- );
730
- const migrationsPattern = new RegExp(
731
- `DROP\\s+TABLE(?:\\s+IF\\s+EXISTS)?(?:\\s+"[^"]*"\\s*\\.)?\\s*"${escapedTable}"`,
732
- "i"
733
- );
766
+ const migrationsPattern = buildMigrationsTableDropPattern(MIGRATIONS_TABLE);
734
767
  const filtered = statements.filter(
735
768
  (stmt) => !migrationsPattern.test(stmt)
736
769
  );
@@ -739,10 +772,7 @@ var pushCommand = new Command4("push").description("Push schema to database (add
739
772
  ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {}
740
773
  });
741
774
  if (options.json) {
742
- const droppedTables = statements.filter((s) => /DROP\s+TABLE/i.test(s)).filter((s) => !migrationsPattern.test(s)).map((s) => {
743
- const m = s.match(/"([^"]+)"\s*(?:CASCADE\s*)?;?\s*$/i);
744
- return m ? m[1] : s;
745
- }).filter((t) => t !== void 0);
775
+ const droppedTables = statements.filter((s) => /DROP\s+TABLE/i.test(s)).filter((s) => !migrationsPattern.test(s)).map(extractDroppedTableName).filter((t) => t !== void 0);
746
776
  console.log(
747
777
  JSON.stringify(
748
778
  {
@@ -762,10 +792,17 @@ var pushCommand = new Command4("push").description("Push schema to database (add
762
792
  );
763
793
  }
764
794
  } else {
765
- const dbModel = await introspect2(pool, {
766
- ...options.schemaName ? { schema: options.schemaName } : {}
767
- });
768
- const diff = compareSchemata2(schemaModel, dbModel);
795
+ const adapter = createPgsqlAdapter2(pool);
796
+ const compareOptions = {
797
+ ...options.schemaName ? { schema: options.schemaName } : {},
798
+ ...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {},
799
+ onWarning: (message) => console.warn(`\u26A0\uFE0F ${message}`)
800
+ };
801
+ const diff = await comparePgsqlDatabaseSchema2(
802
+ adapter,
803
+ schemaModel,
804
+ compareOptions
805
+ );
769
806
  const statements = generateMigrationSQL2(diff, {
770
807
  includeDestructive: false,
771
808
  ...options.schemaName ? { schemaName: options.schemaName } : {}
@@ -802,6 +839,17 @@ var pushCommand = new Command4("push").description("Push schema to database (add
802
839
  const result = await executeDdl(pool, statements, {
803
840
  ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {}
804
841
  });
842
+ if (!options.dryRun) {
843
+ const postApplyDiff = await comparePgsqlDatabaseSchema2(
844
+ adapter,
845
+ schemaModel,
846
+ {
847
+ ...compareOptions,
848
+ previouslyAppliedDiff: diff
849
+ }
850
+ );
851
+ assertNoUnexpectedResidualDrift(postApplyDiff, skippedChanges);
852
+ }
805
853
  if (options.json) {
806
854
  console.log(
807
855
  JSON.stringify(
@@ -837,6 +885,17 @@ var pushCommand = new Command4("push").description("Push schema to database (add
837
885
  }
838
886
  }
839
887
  );
888
+ function buildMigrationsTableDropPattern(tableName) {
889
+ const escaped = tableName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
890
+ return new RegExp(
891
+ `DROP\\s+TABLE(?:\\s+IF\\s+EXISTS)?(?:\\s+"[^"]*"\\s*\\.)?\\s*"${escaped}"`,
892
+ "i"
893
+ );
894
+ }
895
+ function extractDroppedTableName(statement) {
896
+ const match = statement.match(/"([^"]+)"\s*(?:CASCADE\s*)?;?\s*$/iu);
897
+ return match?.[1] ?? statement;
898
+ }
840
899
  function outputResult(statements, options) {
841
900
  if (options.dryRun && !options.json) {
842
901
  console.log(`-- Dry run: ${statements.length} statement(s)
@@ -847,6 +906,30 @@ function outputResult(statements, options) {
847
906
  }
848
907
  }
849
908
  }
909
+ function assertNoUnexpectedResidualDrift(postApplyDiff, skippedChanges) {
910
+ const skippedKeys = new Set(skippedChanges.map(changeIdentity));
911
+ const unexpected = postApplyDiff.changes.filter(
912
+ (change) => !(change.destructive && skippedKeys.has(changeIdentity(change)))
913
+ );
914
+ if (unexpected.length === 0) return;
915
+ throw new Error(
916
+ `Push did not converge after applying DDL; ${unexpected.length} outstanding change(s) remain:
917
+ ` + unexpected.map(formatOutstandingChange).join("\n")
918
+ );
919
+ }
920
+ function changeIdentity(change) {
921
+ return JSON.stringify([
922
+ change.kind,
923
+ change.table,
924
+ change.column ?? null,
925
+ change.details,
926
+ change.destructive
927
+ ]);
928
+ }
929
+ function formatOutstandingChange(change) {
930
+ const target = change.column ? `${change.table}.${change.column}` : change.table || "schema";
931
+ return ` - ${change.details} (${change.kind}: ${target})`;
932
+ }
850
933
 
851
934
  // src/commands/repl.ts
852
935
  import { readFileSync as readFileSync2 } from "fs";
@@ -903,7 +986,7 @@ var replCommand = new Command5("repl").description("Launch interactive REPL for
903
986
  if (options.use) {
904
987
  validateIdentifier(options.use, "schema");
905
988
  }
906
- const dbCasing = options.casing === "snake" ? "snake_case" : options.casing === "camel" ? "camelCase" : options.casing === "none" ? "preserve" : void 0;
989
+ const dbCasing = mapReplCasingOption(options.casing) ?? schema.dbCasing;
907
990
  if (options.eval || options.input) {
908
991
  const { runBatchMode } = await import("./repl/batch.js");
909
992
  const queries = [];
@@ -957,9 +1040,24 @@ var replCommand = new Command5("repl").description("Launch interactive REPL for
957
1040
  process.exit(1);
958
1041
  }
959
1042
  });
1043
+ function mapReplCasingOption(casing) {
1044
+ switch (casing) {
1045
+ case "snake":
1046
+ return "snake_case";
1047
+ case "camel":
1048
+ return "camelCase";
1049
+ case "none":
1050
+ return "preserve";
1051
+ default:
1052
+ return void 0;
1053
+ }
1054
+ }
960
1055
 
961
1056
  // src/commands/verify.ts
962
- import { compareSchemata as compareSchemata3, introspect as introspect3 } from "@dbsp/adapter-pgsql";
1057
+ import {
1058
+ comparePgsqlDatabaseSchema as comparePgsqlDatabaseSchema3,
1059
+ createPgsqlAdapter as createPgsqlAdapter3
1060
+ } from "@dbsp/adapter-pgsql";
963
1061
  import { Command as Command6 } from "commander";
964
1062
 
965
1063
  // src/verifier.ts
@@ -1104,10 +1202,15 @@ var verifyCommand = new Command6("verify").description("Compare schema vs real d
1104
1202
  const schemaModel = loaded.model;
1105
1203
  const { pool } = await createDbConnection(options.db);
1106
1204
  try {
1107
- const dbModel = await introspect3(pool, {
1205
+ const adapter = createPgsqlAdapter3(pool);
1206
+ const diff = await comparePgsqlDatabaseSchema3(adapter, schemaModel, {
1207
+ ...options.schemaName ? { schema: options.schemaName } : {},
1208
+ ...loaded.dbCasing !== void 0 ? { dbCasing: loaded.dbCasing } : {},
1209
+ onWarning: (message) => console.warn(`\u26A0\uFE0F ${message}`)
1210
+ });
1211
+ const dbModel = await adapter.introspect({
1108
1212
  ...options.schemaName ? { schema: options.schemaName } : {}
1109
1213
  });
1110
- const diff = compareSchemata3(schemaModel, dbModel);
1111
1214
  const schemaTables = Array.from(schemaModel.tables.keys());
1112
1215
  const dbTables = Array.from(dbModel.tables.keys());
1113
1216
  const result = verifyFromDiff(diff, schemaTables, dbTables);