turbine-orm 0.40.0 → 0.40.1

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/README.md CHANGED
@@ -602,22 +602,22 @@ Operators for Postgres array columns (`text[]`, `int[]`, etc.).
602
602
  npx turbine <command> [options]
603
603
 
604
604
  Commands:
605
- init Initialize a Turbine project (creates config, dirs, templates)
606
- generate | pull Introspect database and generate TypeScript types + client
607
- push Apply schema-builder definitions to database
608
- migrate create <name> Create a new SQL migration file
609
- migrate create <name> --auto Auto-generate from schema diff
610
- migrate create <name> --from-diff Like --auto, but flag destructive statements
611
- migrate up Apply pending migrations
612
- migrate deploy Apply pending migrations without prompts
613
- migrate down Rollback last migration
614
- migrate status Show applied/pending migrations
615
- seed Run seed file
616
- status Show database schema summary
617
- doctor Check relations for missing FK indexes (--fix emits migration)
618
- studio Launch local Studio web UI (read-only; --write for writes, --demo for a sample DB)
619
- mcp Start read-only MCP server over JSON-RPC stdio
620
- observe Launch local metrics dashboard (requires TURBINE_OBSERVE_URL)
605
+ init Initialize a Turbine project (creates config, dirs, templates)
606
+ generate | pull Introspect database and generate TypeScript types + client
607
+ push Apply schema-builder definitions to database
608
+ migrate create <name> Create a new SQL migration file
609
+ migrate create <name> --auto Auto-generate from schema diff
610
+ migrate create <name> --from-diff Like --auto, but flag destructive statements
611
+ migrate up Apply pending migrations
612
+ migrate deploy Apply pending migrations without prompts
613
+ migrate down Rollback last migration (--step N for last N)
614
+ migrate status Show applied/pending migrations
615
+ seed Run seed file
616
+ status Show database schema summary
617
+ doctor Check relations for missing FK indexes (--fix emits migration)
618
+ studio Launch local Studio web UI (read-only; --write for writes, --demo for a sample DB)
619
+ mcp Start read-only MCP server over JSON-RPC stdio
620
+ observe Launch local metrics dashboard (requires TURBINE_OBSERVE_URL)
621
621
 
622
622
  Options:
623
623
  --url, -u <url> Postgres connection string
@@ -625,6 +625,10 @@ Options:
625
625
  --schema, -s <name> Postgres schema (default: public)
626
626
  --auto Auto-generate migration from schema diff
627
627
  --from-diff Like --auto, but flag destructive statements (migrate create)
628
+ --recipe <name> Scaffold a sanctioned pattern, e.g. backfill (migrate create)
629
+ --step, -n <N> Apply/rollback only N migrations (migrate up / down)
630
+ --allow-drift Bypass checksum validation (migrate up / deploy)
631
+ --allow-destructive Run data-destroying statements without confirmation (up / down / push)
628
632
  --dry-run Show SQL without executing
629
633
  --verbose, -v Detailed output
630
634
  ```
@@ -67,6 +67,7 @@ exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
67
67
  exports.getSeedExecutionPlan = getSeedExecutionPlan;
68
68
  exports.isLoopbackHost = isLoopbackHost;
69
69
  const node_fs_1 = require("node:fs");
70
+ const node_os_1 = require("node:os");
70
71
  const node_path_1 = require("node:path");
71
72
  const node_url_1 = require("node:url");
72
73
  const generate_js_1 = require("../generate.js");
@@ -74,6 +75,7 @@ const index_advisor_js_1 = require("../index-advisor.js");
74
75
  const introspect_js_1 = require("../introspect.js");
75
76
  const schema_sql_js_1 = require("../schema-sql.js");
76
77
  const config_js_1 = require("./config.js");
78
+ const destructive_js_1 = require("./destructive.js");
77
79
  const loader_js_1 = require("./loader.js");
78
80
  const mcp_js_1 = require("./mcp.js");
79
81
  const migrate_js_1 = require("./migrate.js");
@@ -1129,12 +1131,13 @@ async function cmdMigrate(args, config) {
1129
1131
  console.log(` ${(0, ui_js_1.cyan)('status')} Show migration status`);
1130
1132
  (0, ui_js_1.newline)();
1131
1133
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
1132
- console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
1133
- console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Like --auto, but flags destructive statements in the file`);
1134
+ console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff (destructive flagged)`);
1135
+ console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, destructive statements flagged inline`);
1134
1136
  console.log(` ${(0, ui_js_1.cyan)('--recipe <name>')} Scaffold a sanctioned migration pattern`);
1135
1137
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} Number of migrations to apply/rollback`);
1136
1138
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
1137
- console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('migrate up')} ${(0, ui_js_1.dim)('(advanced)')}`);
1139
+ console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting`);
1140
+ console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} ${(0, ui_js_1.dim)('(advanced)')}`);
1138
1141
  (0, ui_js_1.newline)();
1139
1142
  console.log(` ${(0, ui_js_1.bold)('Recipes')} ${(0, ui_js_1.dim)('(--recipe):')}`);
1140
1143
  for (const [key, recipe] of Object.entries(migrate_js_1.MIGRATION_RECIPES)) {
@@ -1217,9 +1220,13 @@ async function cmdMigrateCreate(args, config) {
1217
1220
  }
1218
1221
  diffSpinner.succeed(`Found ${(0, ui_js_1.bold)(String(diff.statements.length))} change(s)`);
1219
1222
  (0, ui_js_1.newline)();
1220
- const upSQL = diff.statements.join('\n');
1221
- const downSQL = diff.reverseStatements.join('\n');
1222
- const file = (0, migrate_js_1.createMigration)(config.migrationsDir, name, { up: upSQL, down: downSQL });
1223
+ // Route through buildDiffMigrationBody so any destructive statement (a lossy
1224
+ // ALTER COLUMN ... TYPE, or a DROP COLUMN for a column removed from the
1225
+ // schema) is flagged inline in the file, matching --from-diff. A
1226
+ // destructive-only diff therefore produces a real, flagged migration instead
1227
+ // of the old "already in sync" false negative.
1228
+ const body = (0, migrate_js_1.buildDiffMigrationBody)(diff);
1229
+ const file = (0, migrate_js_1.createMigration)(config.migrationsDir, name, { up: body.up, down: body.down });
1223
1230
  const relPath = (0, node_path_1.relative)(process.cwd(), file.path);
1224
1231
  (0, ui_js_1.success)(`Created auto-migration: ${(0, ui_js_1.bold)(file.filename)}`);
1225
1232
  (0, ui_js_1.newline)();
@@ -1247,6 +1254,27 @@ async function cmdMigrateCreate(args, config) {
1247
1254
  }
1248
1255
  }
1249
1256
  (0, ui_js_1.newline)();
1257
+ // Loudly flag any destructive statements written into the file (same as
1258
+ // --from-diff). They stay intact so `migrate up` still refuses them by
1259
+ // default, but the operator must know they are there before running.
1260
+ const destructiveCount = body.destructiveUp.length + body.destructiveDown.length;
1261
+ if (destructiveCount > 0) {
1262
+ (0, ui_js_1.warn)(`This migration contains ${(0, ui_js_1.bold)(String(destructiveCount))} DESTRUCTIVE statement(s), flagged in the file.`);
1263
+ for (const h of body.destructiveUp) {
1264
+ console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('UP')} [${h.kind}] ${h.target}`);
1265
+ }
1266
+ for (const h of body.destructiveDown) {
1267
+ console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('DOWN')} [${h.kind}] ${h.target}`);
1268
+ }
1269
+ (0, ui_js_1.newline)();
1270
+ console.log(` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`);
1271
+ (0, ui_js_1.newline)();
1272
+ }
1273
+ if (diff.warnings && diff.warnings.length > 0) {
1274
+ for (const w of diff.warnings)
1275
+ (0, ui_js_1.warn)(w);
1276
+ (0, ui_js_1.newline)();
1277
+ }
1250
1278
  console.log(` ${(0, ui_js_1.dim)('Review the migration, then run:')}`);
1251
1279
  console.log(` ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
1252
1280
  (0, ui_js_1.newline)();
@@ -1417,6 +1445,7 @@ async function cmdMigrateUp(args, config) {
1417
1445
  console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
1418
1446
  }
1419
1447
  }
1448
+ warnOutOfOrder(result.outOfOrder);
1420
1449
  if (result.errors.length > 0) {
1421
1450
  spinner.fail('Migration failed');
1422
1451
  for (const { file, error: msg } of result.errors) {
@@ -1428,58 +1457,101 @@ async function cmdMigrateUp(args, config) {
1428
1457
  }
1429
1458
  (0, ui_js_1.newline)();
1430
1459
  }
1431
- function buildMigrateDeployOptions(_args) {
1460
+ /** Print a one-line warning for each migration applied out of timestamp order. */
1461
+ function warnOutOfOrder(outOfOrder) {
1462
+ if (outOfOrder.length === 0)
1463
+ return;
1464
+ (0, ui_js_1.newline)();
1465
+ for (const o of outOfOrder) {
1466
+ (0, ui_js_1.warn)(`Applied ${(0, ui_js_1.bold)(o.applied)} out of order (older than already-applied ${(0, ui_js_1.bold)(o.newestPrior)}).`);
1467
+ }
1468
+ }
1469
+ function buildMigrateDeployOptions(args) {
1432
1470
  return {
1433
- allowDrift: false,
1471
+ allowDrift: args.allowDrift === true,
1434
1472
  allowDestructive: true,
1435
1473
  step: undefined,
1436
1474
  };
1437
1475
  }
1476
+ /**
1477
+ * Print the itemized, classified destructive-statement report as a NOTICE.
1478
+ * `deploy` proceeds by design (the gate ran at author time), but it must not run
1479
+ * data-destroying SQL in total silence; the notice ends that zero-ceremony hole.
1480
+ */
1481
+ function printDestructiveNotice(offenders) {
1482
+ (0, ui_js_1.warn)('NOTICE: this deploy runs DESTRUCTIVE statement(s):');
1483
+ for (const o of offenders) {
1484
+ console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${o.file}`);
1485
+ for (const h of o.hits) {
1486
+ console.log(` ${(0, ui_js_1.dim)('-')} [${h.kind}] ${h.target} ${(0, ui_js_1.dim)(destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind])}`);
1487
+ }
1488
+ }
1489
+ (0, ui_js_1.newline)();
1490
+ }
1438
1491
  async function cmdMigrateDeploy(args, config) {
1439
1492
  (0, ui_js_1.banner)();
1440
1493
  const url = requireUrl(config);
1441
1494
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
1442
1495
  (0, ui_js_1.label)('Migrations', config.migrationsDir);
1443
1496
  (0, ui_js_1.newline)();
1444
- if (args.dryRun) {
1445
- const spinner = new ui_js_1.Spinner('Checking pending migrations').start();
1446
- const plan = await (0, migrate_js_1.inspectMigrationDeploy)(url, config.migrationsDir);
1447
- if (plan.mismatches.length > 0) {
1448
- spinner.fail('Deploy blocked by migration drift');
1449
- for (const mismatch of plan.mismatches) {
1450
- const reason = mismatch.type === 'missing' ? 'deleted from disk' : 'modified on disk';
1451
- console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${mismatch.name}.sql ${(0, ui_js_1.dim)(`(${reason})`)}`);
1497
+ const spinner = new ui_js_1.Spinner('Checking pending migrations').start();
1498
+ const plan = await (0, migrate_js_1.inspectMigrationDeploy)(url, config.migrationsDir);
1499
+ spinner.stop();
1500
+ // Drift handling: honor --allow-drift exactly like `up`. Without it, block;
1501
+ // with it, warn loudly and proceed.
1502
+ if (plan.mismatches.length > 0) {
1503
+ if (!args.allowDrift) {
1504
+ (0, ui_js_1.error)('Deploy blocked by migration drift');
1505
+ (0, ui_js_1.newline)();
1506
+ for (const line of (0, migrate_js_1.formatChecksumMismatchError)(plan.mismatches).split('\n')) {
1507
+ console.log(` ${line.replace('[turbine] ', '')}`);
1452
1508
  }
1453
1509
  (0, ui_js_1.newline)();
1454
1510
  process.exit(1);
1455
1511
  }
1512
+ (0, ui_js_1.warn)('--allow-drift is set: checksum validation is DISABLED for this deploy.');
1513
+ console.log(` ${(0, ui_js_1.dim)('Applied migrations may have been modified or deleted on disk.')}`);
1514
+ (0, ui_js_1.newline)();
1515
+ }
1516
+ if (args.dryRun) {
1456
1517
  if (plan.pending.length === 0) {
1457
- spinner.succeed('No pending migrations');
1518
+ (0, ui_js_1.info)('No pending migrations');
1458
1519
  (0, ui_js_1.newline)();
1459
1520
  return;
1460
1521
  }
1461
- spinner.succeed(`${(0, ui_js_1.bold)(String(plan.pending.length))} pending migration(s)`);
1522
+ (0, ui_js_1.info)(`${(0, ui_js_1.bold)(String(plan.pending.length))} pending migration(s)`);
1462
1523
  for (const file of plan.pending) {
1463
1524
  console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${file.filename}`);
1464
1525
  }
1526
+ // Surface destructive statements even in a dry run so CI can see them.
1527
+ const destructive = (0, migrate_js_1.collectUpDestructive)(plan.pending);
1528
+ if (destructive.length > 0) {
1529
+ (0, ui_js_1.newline)();
1530
+ printDestructiveNotice(destructive);
1531
+ }
1465
1532
  (0, ui_js_1.newline)();
1466
1533
  return;
1467
1534
  }
1468
- const spinner = new ui_js_1.Spinner('Deploying migrations').start();
1469
- const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir);
1535
+ // Destructive notice before applying (deploy still proceeds by design).
1536
+ const destructive = (0, migrate_js_1.collectUpDestructive)(plan.pending);
1537
+ if (destructive.length > 0)
1538
+ printDestructiveNotice(destructive);
1539
+ const runSpinner = new ui_js_1.Spinner('Deploying migrations').start();
1540
+ const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, { allowDrift: args.allowDrift });
1470
1541
  if (result.applied.length === 0 && result.errors.length === 0) {
1471
- spinner.succeed('0 applied all migrations are up to date');
1542
+ runSpinner.succeed('0 applied, all migrations are up to date');
1472
1543
  (0, ui_js_1.newline)();
1473
1544
  return;
1474
1545
  }
1475
1546
  if (result.applied.length > 0) {
1476
- spinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
1547
+ runSpinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
1477
1548
  for (const file of result.applied) {
1478
1549
  console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
1479
1550
  }
1480
1551
  }
1552
+ warnOutOfOrder(result.outOfOrder);
1481
1553
  if (result.errors.length > 0) {
1482
- spinner.fail('Deploy failed');
1554
+ runSpinner.fail('Deploy failed');
1483
1555
  for (const { file, error: msg } of result.errors) {
1484
1556
  console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${file.filename}`);
1485
1557
  console.log(` ${(0, ui_js_1.dim)(msg)}`);
@@ -1596,8 +1668,17 @@ async function cmdMigrateStatus(_args, config) {
1596
1668
  const pendingCount = statuses.filter((s) => !s.applied).length;
1597
1669
  (0, ui_js_1.info)(`${(0, ui_js_1.bold)(String(appliedCount))} applied, ${pendingCount > 0 ? (0, ui_js_1.yellow)((0, ui_js_1.bold)(String(pendingCount))) : (0, ui_js_1.bold)(String(pendingCount))} pending`);
1598
1670
  (0, ui_js_1.newline)();
1599
- // Check for checksum mismatches
1600
- const driftCount = statuses.filter((s) => s.checksumValid === false).length;
1671
+ // Applied migrations whose file was deleted from disk (distinct from an
1672
+ // on-disk edit). Counted in "applied" above; surfaced with their own banner.
1673
+ const missingCount = statuses.filter((s) => s.missingFile).length;
1674
+ if (missingCount > 0) {
1675
+ (0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(missingCount))} applied migration(s) are missing from disk!`);
1676
+ console.log(` ${(0, ui_js_1.dim)('The history table records them, but their .sql file is gone.')}`);
1677
+ console.log(` ${(0, ui_js_1.dim)('Restore the file(s) before running')} ${(0, ui_js_1.cyan)('migrate up')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('migrate deploy')}${(0, ui_js_1.dim)('.')}`);
1678
+ (0, ui_js_1.newline)();
1679
+ }
1680
+ // Check for checksum mismatches (on-disk edits only; missing files above).
1681
+ const driftCount = statuses.filter((s) => s.checksumValid === false && !s.missingFile).length;
1601
1682
  if (driftCount > 0) {
1602
1683
  (0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(driftCount))} migration(s) have been modified after application!`);
1603
1684
  console.log(` ${(0, ui_js_1.dim)('Applied migrations should be immutable. Modifying them can cause drift.')}`);
@@ -1607,7 +1688,10 @@ async function cmdMigrateStatus(_args, config) {
1607
1688
  const headers = ['Status', 'Migration', 'Applied at'];
1608
1689
  const rows = statuses.map((s) => {
1609
1690
  let status;
1610
- if (s.applied && s.checksumValid === false) {
1691
+ if (s.missingFile) {
1692
+ status = (0, ui_js_1.red)(`${ui_js_1.symbols.warning} Missing file`);
1693
+ }
1694
+ else if (s.applied && s.checksumValid === false) {
1611
1695
  status = (0, ui_js_1.red)(`${ui_js_1.symbols.warning} Drifted`);
1612
1696
  }
1613
1697
  else if (s.applied) {
@@ -1656,21 +1740,40 @@ async function runSeedPlan(plan, config) {
1656
1740
  if (!(0, loader_js_1.canResolveTsx)()) {
1657
1741
  throw new Error('TypeScript seed files require tsx — install tsx or use seed.js/seed.sql.');
1658
1742
  }
1743
+ // The seed runs in a child process, so we cannot observe its callback
1744
+ // directly. Hand it a sentinel path: `defineSeed`'s runner writes the file
1745
+ // only after a callback executes to completion. If the child exits cleanly
1746
+ // but the sentinel never appears, the seed module loaded without running
1747
+ // anything: a silent no-op we must report as a failure, not success.
1748
+ const sentinelDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'turbine-seed-'));
1749
+ const sentinel = (0, node_path_1.join)(sentinelDir, 'ran');
1659
1750
  const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
1660
- execFileSync(plan.command, plan.args, {
1661
- stdio: 'inherit',
1662
- env: {
1663
- ...process.env,
1664
- DATABASE_URL: config.url || process.env.DATABASE_URL,
1665
- },
1666
- });
1751
+ try {
1752
+ execFileSync(plan.command, plan.args, {
1753
+ stdio: 'inherit',
1754
+ env: {
1755
+ ...process.env,
1756
+ DATABASE_URL: config.url || process.env.DATABASE_URL,
1757
+ TURBINE_SEED_SENTINEL: sentinel,
1758
+ },
1759
+ });
1760
+ if (!(0, node_fs_1.existsSync)(sentinel)) {
1761
+ throw new Error('The seed file completed without running a seed callback. ' +
1762
+ 'Make sure it calls defineSeed(async (db) => { ... }) at the top level.');
1763
+ }
1764
+ }
1765
+ finally {
1766
+ (0, node_fs_1.rmSync)(sentinelDir, { recursive: true, force: true });
1767
+ }
1667
1768
  return;
1668
1769
  }
1669
1770
  if (plan.kind === 'js') {
1670
1771
  const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(plan.file).href}`).then(s => __importStar(require(s)));
1671
- if (typeof mod.default === 'function') {
1672
- await mod.default();
1772
+ if (typeof mod.default !== 'function') {
1773
+ throw new Error('The seed file has no callable default export. ' +
1774
+ 'Export your seed with `export default defineSeed(async (db) => { ... })`.');
1673
1775
  }
1776
+ await mod.default();
1674
1777
  return;
1675
1778
  }
1676
1779
  const url = requireUrl(config);
@@ -2263,8 +2366,18 @@ function showHelp() {
2263
2366
  (0, ui_js_1.newline)();
2264
2367
  console.log(` ${(0, ui_js_1.bold)('Migrate options:')}`);
2265
2368
  console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff ${(0, ui_js_1.dim)('(create)')}`);
2369
+ console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, destructive statements flagged ${(0, ui_js_1.dim)('(create)')}`);
2370
+ console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a named migration recipe, e.g. backfill ${(0, ui_js_1.dim)('(create)')}`);
2266
2371
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
2267
- console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('migrate up')} ${(0, ui_js_1.dim)('(advanced)')}`);
2372
+ console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting ${(0, ui_js_1.dim)('(up/down/push)')}`);
2373
+ console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} ${(0, ui_js_1.dim)('(advanced)')}`);
2374
+ (0, ui_js_1.newline)();
2375
+ console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
2376
+ console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
2377
+ console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't scaffold the schema file`);
2378
+ console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't scaffold or run the seed file`);
2379
+ console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't offer to push the schema to the database`);
2380
+ console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't offer to generate the typed client`);
2268
2381
  (0, ui_js_1.newline)();
2269
2382
  console.log(` ${(0, ui_js_1.bold)('Studio / observe options:')}`);
2270
2383
  console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4983 studio, 4984 observe)')}`);
@@ -17,6 +17,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  };
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.MIGRATION_RECIPES = void 0;
20
+ exports.migrationTimestamp = migrationTimestamp;
21
+ exports.collectUpDestructive = collectUpDestructive;
20
22
  exports.parseMigrationFilename = parseMigrationFilename;
21
23
  exports.sanitizeName = sanitizeName;
22
24
  exports.formatTimestamp = formatTimestamp;
@@ -27,6 +29,7 @@ exports.parseMigrationSQL = parseMigrationSQL;
27
29
  exports.buildDiffMigrationBody = buildDiffMigrationBody;
28
30
  exports.createMigration = createMigration;
29
31
  exports.deriveLockId = deriveLockId;
32
+ exports.formatChecksumMismatchError = formatChecksumMismatchError;
30
33
  exports.planMigrationDeploy = planMigrationDeploy;
31
34
  exports.inspectMigrationDeploy = inspectMigrationDeploy;
32
35
  exports.migrateUp = migrateUp;
@@ -41,6 +44,24 @@ const index_js_1 = require("../adapters/index.js");
41
44
  const dialect_js_1 = require("../dialect.js");
42
45
  const errors_js_1 = require("../errors.js");
43
46
  const destructive_js_1 = require("./destructive.js");
47
+ /** Extract the YYYYMMDDHHMMSS timestamp prefix from a migration name, or null. */
48
+ function migrationTimestamp(name) {
49
+ const m = name.match(/^(\d{14})(?:_|$)/);
50
+ return m ? m[1] : null;
51
+ }
52
+ /** Scan a set of migration files' UP sections for data-destroying statements. */
53
+ function collectUpDestructive(files) {
54
+ const offenders = [];
55
+ for (const file of files) {
56
+ const { up } = parseMigrationSQL(file.path);
57
+ if (!up)
58
+ continue;
59
+ const hits = (0, destructive_js_1.scanDestructiveSql)(up);
60
+ if (hits.length > 0)
61
+ offenders.push({ file: file.filename, hits });
62
+ }
63
+ return offenders;
64
+ }
44
65
  // ---------------------------------------------------------------------------
45
66
  // Tracking table management
46
67
  // ---------------------------------------------------------------------------
@@ -472,7 +493,14 @@ function formatChecksumMismatchError(mismatches) {
472
493
  lines.push('');
473
494
  lines.push('Fix one of these:');
474
495
  lines.push(' 1. Restore the file(s) to their original content, OR');
475
- lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down`, OR');
496
+ if (modified.length > 0) {
497
+ // `migrate down` needs the file on disk to read its DOWN section, so it is
498
+ // only a remedy for MODIFIED files, never for deleted ones.
499
+ lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down` (modified files only), OR');
500
+ }
501
+ if (missing.length > 0) {
502
+ lines.push(' (deleted files cannot be rolled back: restore the file, then run `migrate down` if needed), OR');
503
+ }
476
504
  lines.push(' 3. Pass `--allow-drift` to bypass this check (advanced — make sure you know what you are doing).');
477
505
  return lines.join('\n');
478
506
  }
@@ -581,42 +609,43 @@ async function migrateUp(connectionString, migrationsDir, options) {
581
609
  }
582
610
  const applied = await getAppliedMigrations(client, dialect);
583
611
  const appliedNames = new Set(applied.map((m) => m.name));
612
+ // Newest already-applied timestamp. Anything applied below this line is
613
+ // going in out of order (an older migration created/applied after a newer
614
+ // one). Used to surface a warning; never blocks.
615
+ const newestPrior = applied
616
+ .map((m) => ({ ts: migrationTimestamp(m.name), name: m.name }))
617
+ .filter((m) => m.ts !== null)
618
+ .sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0))[0];
584
619
  const allFiles = listMigrationFiles(migrationsDir);
585
620
  let pending = allFiles.filter((f) => !appliedNames.has(f.name));
586
621
  if (options?.step != null && options.step > 0) {
587
622
  pending = pending.slice(0, options.step);
588
623
  }
624
+ // Destructive statements in the pending batch, computed once. Returned in
625
+ // the result regardless of the gate so `deploy` can print a notice even
626
+ // though it proceeds by design.
627
+ const destructive = collectUpDestructive(pending);
589
628
  // Data-loss gate: refuse to run pending migrations containing destructive
590
629
  // statements unless the caller has EXPLICITLY opted in. The CLI layers an
591
630
  // interactive typed confirmation on top of this; programmatic callers must
592
631
  // pass `allowDestructive: true`. Safe-by-default is the whole point — a
593
632
  // DROP TABLE should never run just because a file exists.
594
- if (!options?.allowDestructive) {
595
- const offenders = [];
596
- for (const file of pending) {
597
- const { up } = parseMigrationSQL(file.path);
598
- if (!up)
599
- continue;
600
- const hits = (0, destructive_js_1.scanDestructiveSql)(up);
601
- if (hits.length > 0)
602
- offenders.push({ file: file.filename, hits });
603
- }
604
- if (offenders.length > 0) {
605
- const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
606
- for (const o of offenders) {
607
- lines.push(` ${o.file}`);
608
- for (const h of o.hits) {
609
- lines.push(` - [${h.kind}] ${h.target} — ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
610
- }
633
+ if (!options?.allowDestructive && destructive.length > 0) {
634
+ const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
635
+ for (const o of destructive) {
636
+ lines.push(` ${o.file}`);
637
+ for (const h of o.hits) {
638
+ lines.push(` - [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
611
639
  }
612
- lines.push('');
613
- lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
614
- lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
615
- throw new errors_js_1.MigrationError(lines.join('\n'));
616
640
  }
641
+ lines.push('');
642
+ lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
643
+ lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
644
+ throw new errors_js_1.MigrationError(lines.join('\n'));
617
645
  }
618
646
  const results = [];
619
647
  const errors = [];
648
+ const outOfOrder = [];
620
649
  for (const file of pending) {
621
650
  const { up } = parseMigrationSQL(file.path);
622
651
  if (!up) {
@@ -631,6 +660,11 @@ async function migrateUp(connectionString, migrationsDir, options) {
631
660
  await client.query(dialect.buildMigrationInsertApplied(quotedTrackingTable(dialect)), [file.name, hash]);
632
661
  await client.query('COMMIT');
633
662
  results.push(file);
663
+ // Flag an out-of-order apply: this file's timestamp is older than a
664
+ // migration that was already applied before this run started.
665
+ if (newestPrior && file.timestamp && file.timestamp < newestPrior.ts) {
666
+ outOfOrder.push({ applied: file.filename, newestPrior: `${newestPrior.name}.sql` });
667
+ }
634
668
  }
635
669
  catch (err) {
636
670
  await client.query('ROLLBACK');
@@ -640,7 +674,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
640
674
  break;
641
675
  }
642
676
  }
643
- return { applied: results, errors };
677
+ return { applied: results, errors, destructive, outOfOrder };
644
678
  }
645
679
  finally {
646
680
  await releaseLock(client, lockId, adapter);
@@ -656,7 +690,9 @@ async function migrateUp(connectionString, migrationsDir, options) {
656
690
  */
657
691
  async function migrateDeploy(connectionString, migrationsDir, options) {
658
692
  return migrateUp(connectionString, migrationsDir, {
659
- allowDrift: false,
693
+ // Honor `--allow-drift` on deploy exactly as `up` does: deploy's own drift
694
+ // error recommends this flag, so it must actually bypass the checksum block.
695
+ allowDrift: options?.allowDrift === true,
660
696
  allowDestructive: true,
661
697
  adapter: options?.adapter,
662
698
  dialect: options?.dialect,
@@ -715,7 +751,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
715
751
  for (const o of offenders) {
716
752
  lines.push(` ${o.file}`);
717
753
  for (const h of o.hits) {
718
- lines.push(` - [${h.kind}] ${h.target} ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
754
+ lines.push(` - [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
719
755
  }
720
756
  }
721
757
  lines.push('');
@@ -777,7 +813,8 @@ async function migrateStatus(connectionString, migrationsDir, options) {
777
813
  const applied = await getAppliedMigrations(client, dialect);
778
814
  const appliedMap = new Map(applied.map((m) => [m.name, m]));
779
815
  const allFiles = listMigrationFiles(migrationsDir);
780
- return allFiles.map((file) => {
816
+ const fileNames = new Set(allFiles.map((f) => f.name));
817
+ const fromFiles = allFiles.map((file) => {
781
818
  const record = appliedMap.get(file.name);
782
819
  let checksumValid;
783
820
  if (record) {
@@ -792,6 +829,25 @@ async function migrateStatus(connectionString, migrationsDir, options) {
792
829
  checksumValid,
793
830
  };
794
831
  });
832
+ // Applied migrations whose file was deleted from disk. up/deploy already
833
+ // catch this as drift; status must not silently drop them from history.
834
+ const missing = applied
835
+ .filter((m) => !fileNames.has(m.name))
836
+ .map((m) => ({
837
+ file: parseMigrationFilename(`${m.name}.sql`) ?? {
838
+ filename: `${m.name}.sql`,
839
+ path: '',
840
+ name: m.name,
841
+ timestamp: '',
842
+ },
843
+ applied: true,
844
+ appliedAt: m.applied_at,
845
+ checksumValid: false,
846
+ missingFile: true,
847
+ }));
848
+ // Keep the overall list in timestamp order so a deleted entry appears where
849
+ // it belongs in history, not tacked on at the end.
850
+ return [...fromFiles, ...missing].sort((a, b) => a.file.name < b.file.name ? -1 : a.file.name > b.file.name ? 1 : 0);
795
851
  }
796
852
  finally {
797
853
  await client.end();
@@ -839,14 +839,23 @@ async function schemaDiff(schema, connectionString) {
839
839
  }
840
840
  }
841
841
  }
842
- // Check for columns in DB that are not in schema
842
+ // Check for columns in DB that are not in schema.
843
+ // The DROP COLUMN statement stays IN `result.statements` so the destructive
844
+ // gate (findDestructivePushStatements / scanDestructiveSql, and the migrate
845
+ // up gate) can see it and refuse by default. Safety is enforced by that
846
+ // gate, not by hiding the statement: withholding it here previously made
847
+ // `push` report "already in sync" for a destructive-only diff and print a
848
+ // false "Altered" for a mixed one, since the 0.36 guard never received it.
849
+ // No reverse is emitted (a dropped column and its data cannot be recreated
850
+ // automatically), so diff-generated migrations fall back to the documented
851
+ // irreversible-DOWN placeholder for this change.
843
852
  for (const dbColName of Object.keys(dbCols)) {
844
853
  const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
845
854
  if (!hasField) {
846
855
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
847
- const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" add it back manually`;
856
+ const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}"; add it back manually`;
848
857
  alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
849
- // Don't auto-add drops to statements for safety — user must opt in
858
+ result.statements.push(sql);
850
859
  }
851
860
  }
852
861
  if (alterDef.columns.length > 0) {