turbine-orm 0.77.1 → 0.78.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.
Files changed (93) hide show
  1. package/README.md +13 -9
  2. package/dist/cjs/cli/config.d.ts +7 -1
  3. package/dist/cjs/cli/config.js +11 -2
  4. package/dist/cjs/cli/destructive.d.ts +1 -1
  5. package/dist/cjs/cli/destructive.js +307 -9
  6. package/dist/cjs/cli/index.js +252 -42
  7. package/dist/cjs/cli/mcp.d.ts +23 -0
  8. package/dist/cjs/cli/mcp.js +190 -152
  9. package/dist/cjs/cli/migrate.d.ts +243 -3
  10. package/dist/cjs/cli/migrate.js +432 -43
  11. package/dist/cjs/cli/sql-statements.js +27 -0
  12. package/dist/cjs/cli/studio.d.ts +0 -1
  13. package/dist/cjs/cli/studio.js +9 -7
  14. package/dist/cjs/client.d.ts +8 -1
  15. package/dist/cjs/client.js +7 -0
  16. package/dist/cjs/connection-url.d.ts +82 -0
  17. package/dist/cjs/connection-url.js +187 -1
  18. package/dist/cjs/errors.d.ts +112 -12
  19. package/dist/cjs/errors.js +558 -114
  20. package/dist/cjs/generate.js +47 -15
  21. package/dist/cjs/index.d.ts +1 -1
  22. package/dist/cjs/introspect.d.ts +33 -0
  23. package/dist/cjs/introspect.js +54 -1
  24. package/dist/cjs/mssql.js +21 -1
  25. package/dist/cjs/nested-write.js +85 -14
  26. package/dist/cjs/pipeline-submittable.d.ts +2 -0
  27. package/dist/cjs/pipeline-submittable.js +88 -3
  28. package/dist/cjs/pipeline.js +13 -1
  29. package/dist/cjs/powdb-introspect.d.ts +5 -1
  30. package/dist/cjs/powdb-introspect.js +5 -1
  31. package/dist/cjs/powql.d.ts +14 -0
  32. package/dist/cjs/powql.js +44 -4
  33. package/dist/cjs/prisma-compat.js +95 -8
  34. package/dist/cjs/query/aggregates.js +56 -6
  35. package/dist/cjs/query/builder.d.ts +76 -13
  36. package/dist/cjs/query/builder.js +188 -58
  37. package/dist/cjs/query/compound-unique.d.ts +76 -0
  38. package/dist/cjs/query/compound-unique.js +129 -0
  39. package/dist/cjs/query/index.d.ts +1 -1
  40. package/dist/cjs/query/types.d.ts +65 -11
  41. package/dist/cjs/query/where.d.ts +85 -19
  42. package/dist/cjs/query/where.js +262 -47
  43. package/dist/cjs/query/writes.d.ts +11 -2
  44. package/dist/cjs/query/writes.js +116 -21
  45. package/dist/cjs/seed.d.ts +16 -0
  46. package/dist/cjs/seed.js +16 -0
  47. package/dist/cli/config.d.ts +7 -1
  48. package/dist/cli/config.js +11 -2
  49. package/dist/cli/destructive.d.ts +1 -1
  50. package/dist/cli/destructive.js +307 -9
  51. package/dist/cli/index.js +254 -44
  52. package/dist/cli/mcp.d.ts +23 -0
  53. package/dist/cli/mcp.js +187 -150
  54. package/dist/cli/migrate.d.ts +243 -3
  55. package/dist/cli/migrate.js +423 -45
  56. package/dist/cli/sql-statements.js +27 -0
  57. package/dist/cli/studio.d.ts +0 -1
  58. package/dist/cli/studio.js +10 -7
  59. package/dist/client.d.ts +8 -1
  60. package/dist/client.js +7 -0
  61. package/dist/connection-url.d.ts +82 -0
  62. package/dist/connection-url.js +183 -0
  63. package/dist/errors.d.ts +112 -12
  64. package/dist/errors.js +558 -114
  65. package/dist/generate.js +47 -15
  66. package/dist/index.d.ts +1 -1
  67. package/dist/introspect.d.ts +33 -0
  68. package/dist/introspect.js +53 -1
  69. package/dist/mssql.js +21 -1
  70. package/dist/nested-write.js +85 -14
  71. package/dist/pipeline-submittable.d.ts +2 -0
  72. package/dist/pipeline-submittable.js +87 -3
  73. package/dist/pipeline.js +14 -2
  74. package/dist/powdb-introspect.d.ts +5 -1
  75. package/dist/powdb-introspect.js +5 -1
  76. package/dist/powql.d.ts +14 -0
  77. package/dist/powql.js +45 -5
  78. package/dist/prisma-compat.js +96 -9
  79. package/dist/query/aggregates.js +56 -6
  80. package/dist/query/builder.d.ts +76 -13
  81. package/dist/query/builder.js +188 -58
  82. package/dist/query/compound-unique.d.ts +76 -0
  83. package/dist/query/compound-unique.js +126 -1
  84. package/dist/query/index.d.ts +1 -1
  85. package/dist/query/types.d.ts +65 -11
  86. package/dist/query/where.d.ts +85 -19
  87. package/dist/query/where.js +260 -47
  88. package/dist/query/writes.d.ts +11 -2
  89. package/dist/query/writes.js +117 -22
  90. package/dist/seed.d.ts +16 -0
  91. package/dist/seed.js +16 -0
  92. package/package.json +3 -3
  93. package/skills/turbine-orm/SKILL.md +37 -10
@@ -93,6 +93,7 @@ const node_os_1 = require("node:os");
93
93
  const node_path_1 = require("node:path");
94
94
  const node_url_1 = require("node:url");
95
95
  const connection_url_js_1 = require("../connection-url.js");
96
+ const errors_js_1 = require("../errors.js");
96
97
  const generate_js_1 = require("../generate.js");
97
98
  const index_advisor_js_1 = require("../index-advisor.js");
98
99
  const index_stats_js_1 = require("../index-stats.js");
@@ -362,6 +363,8 @@ function parseArgs(argv = process.argv.slice(2)) {
362
363
  // command instead, which is the actual problem.
363
364
  const accepted = acceptedFlagTokens(result.command);
364
365
  const unknownFlags = [];
366
+ /** The raw `--step` operand, judged after the loop. Absent means unset. */
367
+ let stepRaw;
365
368
  for (; i < args.length; i++) {
366
369
  const arg = args[i];
367
370
  const next = args[i + 1];
@@ -400,10 +403,17 @@ function parseArgs(argv = process.argv.slice(2)) {
400
403
  i++;
401
404
  break;
402
405
  case '--step':
403
- case '-n':
404
- result.step = next ? parseInt(next, 10) : undefined;
406
+ case '-n': {
407
+ // The VALUE is captured here and JUDGED after the loop, see
408
+ // `stepRaw` below. `--step -1` reached `applied.reverse().slice(0, -1)`,
409
+ // which is "every migration except the oldest" and began tearing down
410
+ // the history newest-first; `--step 0` and `--step abc` (NaN) were
411
+ // silent no-ops that look like a successful rollback. A count of things
412
+ // to do is a positive integer or it is a mistake.
413
+ stepRaw = next ?? '';
405
414
  i++;
406
415
  break;
416
+ }
407
417
  case '--dry-run':
408
418
  result.dryRun = true;
409
419
  break;
@@ -566,6 +576,21 @@ function parseArgs(argv = process.argv.slice(2)) {
566
576
  // are two lines of one error rather than two runs of the CLI.
567
577
  if (unknownFlags.length > 0)
568
578
  failUnknownFlags(result.command, unknownFlags);
579
+ // Flag VALUES are judged last, and only for a command that exists. Both halves
580
+ // are the fix for one report: `turbine genrate --step 0` used to complain
581
+ // about `--step` and never mention the misspelled command, because the check
582
+ // ran mid-loop and threw past `main()`'s dispatch. And it threw a bare
583
+ // `ValidationError` rather than calling `failArg`, so it printed one unstyled
584
+ // sentence with no banner and no hint, which reads like an internal crash.
585
+ // `accepted === undefined` means the command is unknown; main() reports that,
586
+ // which is the actual problem.
587
+ if (stepRaw !== undefined && accepted !== undefined) {
588
+ const parsed = /^\d+$/.test(stepRaw.trim()) ? Number.parseInt(stepRaw, 10) : Number.NaN;
589
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
590
+ failArg(`${(0, ui_js_1.cyan)('--step, -n')} expects a positive whole number of migrations, got ${(0, ui_js_1.bold)(stepRaw === '' ? '(nothing)' : `"${stepRaw}"`)}.`, 'It is a COUNT of migrations to apply or roll back, so the smallest useful value is 1.', 'Example: npx turbine migrate down --step 2');
591
+ }
592
+ result.step = parsed;
593
+ }
569
594
  return result;
570
595
  }
571
596
  // ---------------------------------------------------------------------------
@@ -1348,6 +1373,7 @@ async function runInitGenerate(config, url) {
1348
1373
  const spinner = new ui_js_1.Spinner('Introspecting database').start();
1349
1374
  try {
1350
1375
  const schema = await (0, introspect_js_1.introspect)({
1376
+ keepColumnNames: config.keepColumnNames,
1351
1377
  connectionString: url,
1352
1378
  schema: config.schema,
1353
1379
  include: config.include.length ? config.include : undefined,
@@ -1612,7 +1638,7 @@ async function cmdInit(args, config) {
1612
1638
  }
1613
1639
  switch (step.id) {
1614
1640
  case 'config': {
1615
- (0, node_fs_1.writeFileSync)('turbine.config.ts', (0, config_js_1.configTemplate)(args.url ?? undefined), 'utf-8');
1641
+ (0, node_fs_1.writeFileSync)('turbine.config.ts', (0, config_js_1.configTemplate)(args.url ?? undefined, config.schema), 'utf-8');
1616
1642
  (0, ui_js_1.success)(state.configExists ? `Overwrote ${(0, ui_js_1.cyan)('turbine.config.ts')}` : `Created ${(0, ui_js_1.cyan)('turbine.config.ts')}`);
1617
1643
  tsFilesWritten.push('turbine.config.ts');
1618
1644
  // A `--url` with a password is never inlined by configTemplate, so the
@@ -1775,6 +1801,7 @@ async function cmdGenerate(args, config) {
1775
1801
  const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
1776
1802
  const skippedInternalTables = [];
1777
1803
  const schema = await (0, introspect_js_1.introspect)({
1804
+ keepColumnNames: config.keepColumnNames,
1778
1805
  connectionString: url,
1779
1806
  schema: config.schema,
1780
1807
  include: config.include.length ? config.include : undefined,
@@ -1975,6 +2002,7 @@ async function cmdMigrateFromPrisma(args, config) {
1975
2002
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
1976
2003
  const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
1977
2004
  schemaMeta = await (0, introspect_js_1.introspect)({
2005
+ keepColumnNames: config.keepColumnNames,
1978
2006
  connectionString: url,
1979
2007
  // The Postgres NAMESPACE is fixed to `public` here (`--schema` names the
1980
2008
  // Prisma file, not the namespace).
@@ -2224,7 +2252,7 @@ async function cmdMigrate(args, config) {
2224
2252
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} Number of migrations to apply/rollback`);
2225
2253
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
2226
2254
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting`);
2227
- 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)')}`);
2255
+ console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums on ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} ${(0, ui_js_1.dim)('(advanced)')}`);
2228
2256
  (0, ui_js_1.newline)();
2229
2257
  console.log(` ${(0, ui_js_1.bold)('Recipes')} ${(0, ui_js_1.dim)('(--recipe):')}`);
2230
2258
  for (const [key, recipe] of Object.entries(migrate_js_1.MIGRATION_RECIPES)) {
@@ -2515,6 +2543,7 @@ async function cmdMigrateUp(args, config) {
2515
2543
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2516
2544
  (0, ui_js_1.label)('Migrations', config.migrationsDir);
2517
2545
  (0, ui_js_1.newline)();
2546
+ warnIgnoredMigrationFiles(config.migrationsDir);
2518
2547
  const allFiles = (0, migrate_js_1.listMigrationFiles)(config.migrationsDir);
2519
2548
  if (allFiles.length === 0) {
2520
2549
  (0, ui_js_1.warn)('No migration files found.');
@@ -2523,19 +2552,29 @@ async function cmdMigrateUp(args, config) {
2523
2552
  return;
2524
2553
  }
2525
2554
  if (args.dryRun) {
2526
- const status = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir);
2555
+ const status = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir, { schema: config.schema });
2527
2556
  let pending = status.filter((st) => !st.applied).map((st) => st.file);
2528
2557
  if (args.step != null && args.step > 0)
2529
2558
  pending = pending.slice(0, args.step);
2559
+ // The same pre-flight the real run does, by calling the same two functions
2560
+ // rather than re-deriving their rules. They live inside `migrateUp`, which a
2561
+ // dry run never reaches, so `--dry-run` used to green-light a batch the real
2562
+ // command refuses outright: an empty-UP migration previewed as "would apply
2563
+ // 1 migration" and exit 0, and the deploy that followed on merge failed.
2564
+ // A preview that is quieter than the run it previews is not a preview.
2565
+ (0, migrate_js_1.assertNoEmbeddedTransactions)(pending, 'up');
2566
+ (0, migrate_js_1.assertUpHasStatements)(pending);
2530
2567
  printMigrationDryRun(pending, 'up');
2531
2568
  return;
2532
2569
  }
2533
2570
  // Big, loud warning when bypassing drift detection, this is a deliberately
2534
2571
  // dangerous operation and the user should see it on every invocation.
2535
2572
  if (args.allowDrift) {
2536
- (0, ui_js_1.warn)('--allow-drift is set, checksum validation is DISABLED for this run.');
2537
- console.log(` ${(0, ui_js_1.dim)('Applied migrations may have been modified or deleted on disk.')}`);
2538
- console.log(` ${(0, ui_js_1.dim)('Proceed only if you are intentionally rewriting migration history.')}`);
2573
+ (0, ui_js_1.warn)('--allow-drift is set: a drifted migration will be RE-BASELINED, not skipped.');
2574
+ console.log(` ${(0, ui_js_1.dim)('Checksum validation still runs, and its result is written back to the history')}`);
2575
+ console.log(` ${(0, ui_js_1.dim)("table: each modified file's stored checksum becomes its current content.")}`);
2576
+ console.log(` ${(0, ui_js_1.dim)('The SQL that already ran is NOT re-run. Proceed only if you are')}`);
2577
+ console.log(` ${(0, ui_js_1.dim)('intentionally rewriting migration history.')}`);
2539
2578
  (0, ui_js_1.newline)();
2540
2579
  }
2541
2580
  if (args.allowDestructive) {
@@ -2561,6 +2600,7 @@ async function cmdMigrateUp(args, config) {
2561
2600
  allowDrift: args.allowDrift,
2562
2601
  allowDestructive: args.allowDestructive,
2563
2602
  onNoTransaction,
2603
+ schema: config.schema,
2564
2604
  });
2565
2605
  }
2566
2606
  catch (err) {
@@ -2578,19 +2618,26 @@ async function cmdMigrateUp(args, config) {
2578
2618
  allowDrift: args.allowDrift,
2579
2619
  allowDestructive: true,
2580
2620
  onNoTransaction,
2621
+ schema: config.schema,
2581
2622
  });
2582
2623
  }
2583
- if (result.applied.length === 0 && result.errors.length === 0) {
2584
- spinner.succeed('All migrations are up to date');
2585
- (0, ui_js_1.newline)();
2586
- return;
2587
- }
2588
2624
  if (result.applied.length > 0) {
2589
2625
  spinner.succeed(`Applied ${(0, ui_js_1.bold)(String(result.applied.length))} migration(s)`);
2590
2626
  for (const file of result.applied) {
2591
2627
  console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
2592
2628
  }
2593
2629
  }
2630
+ else if (result.errors.length === 0) {
2631
+ spinner.succeed('All migrations are up to date');
2632
+ }
2633
+ // Reported on EVERY path, including the one where nothing was applied. That is
2634
+ // `--allow-drift`'s most common case by far (you edited an already-applied
2635
+ // file, so nothing is pending), and it used to sit below an early return: the
2636
+ // stored checksum was rewritten and the terminal said everything was up to
2637
+ // date. A write to migration history announced as a no-op is worse than no
2638
+ // report at all, because the reader now believes the file and the database
2639
+ // still disagree.
2640
+ reportRebaselined(result.rebaselined);
2594
2641
  warnOutOfOrder(result.outOfOrder);
2595
2642
  if (result.errors.length > 0) {
2596
2643
  spinner.fail('Migration failed');
@@ -2603,6 +2650,40 @@ async function cmdMigrateUp(args, config) {
2603
2650
  }
2604
2651
  (0, ui_js_1.newline)();
2605
2652
  }
2653
+ /**
2654
+ * Report each applied migration whose stored checksum was re-baselined to the
2655
+ * file's current content, naming both hashes so the change is auditable from
2656
+ * the terminal output alone.
2657
+ */
2658
+ function reportRebaselined(rebaselined) {
2659
+ if (rebaselined.length === 0)
2660
+ return;
2661
+ (0, ui_js_1.newline)();
2662
+ (0, ui_js_1.warn)(`Re-baselined ${(0, ui_js_1.bold)(String(rebaselined.length))} migration checksum(s) to the current file content:`);
2663
+ for (const r of rebaselined) {
2664
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.arrowRight)} ${r.name}.sql ${(0, ui_js_1.dim)(`${r.from.slice(0, 12)} -> ${r.to.slice(0, 12)}`)}`);
2665
+ }
2666
+ console.log(` ${(0, ui_js_1.dim)('The history table now records what is on disk, so future runs need no --allow-drift.')}`);
2667
+ (0, ui_js_1.newline)();
2668
+ }
2669
+ /**
2670
+ * Warn about `.sql` files in the migrations directory that the runner will
2671
+ * never see, because their name is not `YYYYMMDDHHMMSS_<name>.sql`.
2672
+ *
2673
+ * They were skipped in silence by `status`, `up` and `deploy` alike, so a
2674
+ * hand-named migration simply never ran and nothing said so.
2675
+ */
2676
+ function warnIgnoredMigrationFiles(migrationsDir) {
2677
+ const ignored = (0, migrate_js_1.listIgnoredSqlFiles)(migrationsDir);
2678
+ if (ignored.length === 0)
2679
+ return;
2680
+ (0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(ignored.length))} .sql file(s) in ${migrationsDir} are NOT migrations and will never run:`);
2681
+ for (const name of ignored)
2682
+ console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${name}`);
2683
+ console.log(` ${(0, ui_js_1.dim)('A migration filename must be')} ${(0, ui_js_1.cyan)('YYYYMMDDHHMMSS_name.sql')}${(0, ui_js_1.dim)('.')}`);
2684
+ console.log(` ${(0, ui_js_1.dim)('Rename the file(s), or create them with')} ${(0, ui_js_1.cyan)('npx turbine migrate create <name>')}${(0, ui_js_1.dim)('.')}`);
2685
+ (0, ui_js_1.newline)();
2686
+ }
2606
2687
  /** Print a one-line warning for each migration applied out of timestamp order. */
2607
2688
  function warnOutOfOrder(outOfOrder) {
2608
2689
  if (outOfOrder.length === 0)
@@ -2640,8 +2721,9 @@ async function cmdMigrateDeploy(args, config) {
2640
2721
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
2641
2722
  (0, ui_js_1.label)('Migrations', config.migrationsDir);
2642
2723
  (0, ui_js_1.newline)();
2724
+ warnIgnoredMigrationFiles(config.migrationsDir);
2643
2725
  const spinner = new ui_js_1.Spinner('Checking pending migrations').start();
2644
- const plan = await (0, migrate_js_1.inspectMigrationDeploy)(url, config.migrationsDir);
2726
+ const plan = await (0, migrate_js_1.inspectMigrationDeploy)(url, config.migrationsDir, { schema: config.schema });
2645
2727
  spinner.stop();
2646
2728
  // Drift handling: honor --allow-drift exactly like `up`. Without it, block;
2647
2729
  // with it, warn loudly and proceed.
@@ -2655,8 +2737,10 @@ async function cmdMigrateDeploy(args, config) {
2655
2737
  (0, ui_js_1.errorLine)();
2656
2738
  process.exit(1);
2657
2739
  }
2658
- (0, ui_js_1.warn)('--allow-drift is set: checksum validation is DISABLED for this deploy.');
2659
- console.log(` ${(0, ui_js_1.dim)('Applied migrations may have been modified or deleted on disk.')}`);
2740
+ (0, ui_js_1.warn)('--allow-drift is set: a drifted migration will be RE-BASELINED, not skipped.');
2741
+ console.log(` ${(0, ui_js_1.dim)('Checksum validation still runs, and its result is written back to the history')}`);
2742
+ console.log(` ${(0, ui_js_1.dim)("table: each modified file's stored checksum becomes its current content.")}`);
2743
+ console.log(` ${(0, ui_js_1.dim)('The SQL that already ran is NOT re-run.')}`);
2660
2744
  (0, ui_js_1.newline)();
2661
2745
  }
2662
2746
  if (args.dryRun) {
@@ -2665,10 +2749,21 @@ async function cmdMigrateDeploy(args, config) {
2665
2749
  (0, ui_js_1.newline)();
2666
2750
  return;
2667
2751
  }
2752
+ // The same pre-flight the real run does, by calling the same two functions
2753
+ // rather than re-deriving their rules. They live inside `migrateUp`, which a
2754
+ // dry run never reaches, so `--dry-run` used to green-light a batch the real
2755
+ // command refuses outright: an empty-UP migration previewed as "would apply
2756
+ // 1 migration" and exit 0, and the deploy that followed on merge failed.
2757
+ // A preview that is quieter than the run it previews is not a preview.
2758
+ (0, migrate_js_1.assertNoEmbeddedTransactions)(plan.pending, 'up');
2759
+ (0, migrate_js_1.assertUpHasStatements)(plan.pending);
2668
2760
  (0, ui_js_1.info)(`${(0, ui_js_1.bold)(String(plan.pending.length))} pending migration(s)`);
2669
2761
  for (const file of plan.pending) {
2670
2762
  console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${file.filename}`);
2671
2763
  }
2764
+ // The real deploy warns about out-of-order applies; a preview that is
2765
+ // quieter than the run it previews is not a preview.
2766
+ warnOutOfOrder(plan.outOfOrder);
2672
2767
  // Surface destructive statements even in a dry run so CI can see them.
2673
2768
  const destructive = (0, migrate_js_1.collectUpDestructive)(plan.pending);
2674
2769
  if (destructive.length > 0) {
@@ -2683,18 +2778,24 @@ async function cmdMigrateDeploy(args, config) {
2683
2778
  if (destructive.length > 0)
2684
2779
  printDestructiveNotice(destructive);
2685
2780
  const runSpinner = new ui_js_1.Spinner('Deploying migrations').start();
2686
- const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, { allowDrift: args.allowDrift });
2687
- if (result.applied.length === 0 && result.errors.length === 0) {
2688
- runSpinner.succeed('0 applied, all migrations are up to date');
2689
- (0, ui_js_1.newline)();
2690
- return;
2691
- }
2781
+ const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, { allowDrift: args.allowDrift, schema: config.schema });
2692
2782
  if (result.applied.length > 0) {
2693
2783
  runSpinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
2694
2784
  for (const file of result.applied) {
2695
2785
  console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
2696
2786
  }
2697
2787
  }
2788
+ else if (result.errors.length === 0) {
2789
+ runSpinner.succeed('0 applied, all migrations are up to date');
2790
+ }
2791
+ // Reported on EVERY path, including the one where nothing was applied. That is
2792
+ // `--allow-drift`'s most common case by far (you edited an already-applied
2793
+ // file, so nothing is pending), and it used to sit below an early return: the
2794
+ // stored checksum was rewritten and the terminal said everything was up to
2795
+ // date. A write to migration history announced as a no-op is worse than no
2796
+ // report at all, because the reader now believes the file and the database
2797
+ // still disagree.
2798
+ reportRebaselined(result.rebaselined);
2698
2799
  warnOutOfOrder(result.outOfOrder);
2699
2800
  if (result.errors.length > 0) {
2700
2801
  runSpinner.fail('Deploy failed');
@@ -2752,11 +2853,24 @@ async function cmdMigrateDown(args, config) {
2752
2853
  (0, ui_js_1.label)('Migrations', config.migrationsDir);
2753
2854
  (0, ui_js_1.newline)();
2754
2855
  if (args.dryRun) {
2755
- const status = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir);
2756
- // Rollback order is newest-applied first, and a migration whose file is
2757
- // missing has no DOWN section to show.
2758
- const applied = status.filter((st) => st.applied && !st.missingFile).map((st) => st.file);
2759
- printMigrationDryRun(applied.reverse().slice(0, args.step ?? 1), 'down');
2856
+ // Computed by the RUNNER'S rule (planMigrationRollback), not re-derived
2857
+ // from `status`. The old derivation sorted by filename and dropped applied
2858
+ // migrations whose file is missing, so with a deleted file it named the
2859
+ // migration AFTER the gap while the real run stopped at the gap.
2860
+ const plan = await (0, migrate_js_1.planMigrationRollback)(url, config.migrationsDir, {
2861
+ step: args.step ?? 1,
2862
+ schema: config.schema,
2863
+ // The real rollback refuses on drift unless this is set, so the preview
2864
+ // has to be told too: without it a deleted migration file previewed as
2865
+ // "would roll back 1 migration" while `migrate down` refused with E006.
2866
+ allowDrift: args.allowDrift,
2867
+ });
2868
+ printMigrationDryRun(plan.toRollback, 'down');
2869
+ if (plan.stoppedAt) {
2870
+ (0, ui_js_1.warn)(`Rollback would STOP at ${(0, ui_js_1.bold)(`${plan.stoppedAt.name}.sql`)}: ${plan.stoppedAt.error}`);
2871
+ console.log(` ${(0, ui_js_1.dim)('A rollback batch is strictly LIFO with no gaps, so nothing after it would run.')}`);
2872
+ (0, ui_js_1.newline)();
2873
+ }
2760
2874
  return;
2761
2875
  }
2762
2876
  const spinner = new ui_js_1.Spinner('Rolling back migration(s)').start();
@@ -2765,6 +2879,8 @@ async function cmdMigrateDown(args, config) {
2765
2879
  result = await (0, migrate_js_1.migrateDown)(url, config.migrationsDir, {
2766
2880
  step: args.step ?? 1,
2767
2881
  allowDestructive: args.allowDestructive,
2882
+ allowDrift: args.allowDrift,
2883
+ schema: config.schema,
2768
2884
  });
2769
2885
  }
2770
2886
  catch (err) {
@@ -2780,19 +2896,27 @@ async function cmdMigrateDown(args, config) {
2780
2896
  result = await (0, migrate_js_1.migrateDown)(url, config.migrationsDir, {
2781
2897
  step: args.step ?? 1,
2782
2898
  allowDestructive: true,
2899
+ allowDrift: args.allowDrift,
2900
+ schema: config.schema,
2783
2901
  });
2784
2902
  }
2785
- if (result.rolledBack.length === 0 && result.errors.length === 0) {
2786
- spinner.succeed('No migrations to roll back');
2787
- (0, ui_js_1.newline)();
2788
- return;
2789
- }
2790
2903
  if (result.rolledBack.length > 0) {
2791
2904
  spinner.succeed(`Rolled back ${(0, ui_js_1.bold)(String(result.rolledBack.length))} migration(s)`);
2792
2905
  for (const file of result.rolledBack) {
2793
2906
  console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.arrowRight)} ${file.filename}`);
2794
2907
  }
2795
2908
  }
2909
+ else if (result.errors.length === 0) {
2910
+ spinner.succeed('No migrations to roll back');
2911
+ }
2912
+ // Reported on EVERY path, including the one where nothing was applied. That is
2913
+ // `--allow-drift`'s most common case by far (you edited an already-applied
2914
+ // file, so nothing is pending), and it used to sit below an early return: the
2915
+ // stored checksum was rewritten and the terminal said everything was up to
2916
+ // date. A write to migration history announced as a no-op is worse than no
2917
+ // report at all, because the reader now believes the file and the database
2918
+ // still disagree.
2919
+ reportRebaselined(result.rebaselined);
2796
2920
  if (result.errors.length > 0) {
2797
2921
  spinner.fail('Rollback failed');
2798
2922
  for (const { file, error: msg } of result.errors) {
@@ -2817,7 +2941,8 @@ async function cmdMigrateStatus(_args, config) {
2817
2941
  (0, ui_js_1.newline)();
2818
2942
  return;
2819
2943
  }
2820
- const statuses = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir);
2944
+ warnIgnoredMigrationFiles(config.migrationsDir);
2945
+ const statuses = await (0, migrate_js_1.migrateStatus)(url, config.migrationsDir, { schema: config.schema });
2821
2946
  const appliedCount = statuses.filter((s) => s.applied).length;
2822
2947
  const pendingCount = statuses.filter((s) => !s.applied).length;
2823
2948
  (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`);
@@ -2885,10 +3010,61 @@ function getSeedExecutionPlan(seedFile) {
2885
3010
  }
2886
3011
  throw new Error(`Unsupported seed file extension: ${ext || '(none)'}. Use seed.ts, seed.js, or seed.sql.`);
2887
3012
  }
3013
+ /**
3014
+ * The seed's pinned connection string, refusing a configured schema that does
3015
+ * not exist before any seed kind runs.
3016
+ *
3017
+ * `migrate` gained that guard and `seed` did not, because the seed reached
3018
+ * `connectionStringForSchema` directly instead of going through the migration
3019
+ * runner's connect helper. So seeding a project whose schema was missing (a
3020
+ * typo, or a fresh database nobody had created it in) failed with a bare
3021
+ * `relation "..." does not exist`: no error code, no schema name, no mention of
3022
+ * search_path or turbine.config.ts. That is verbatim the failure the guard
3023
+ * exists to prevent.
3024
+ *
3025
+ * Checked ONCE here rather than in each of the three seed kinds, because that
3026
+ * is the only place all three still share: the `.sql` kind opens its own client
3027
+ * below, while the `.ts` and `.js` kinds connect somewhere this process cannot
3028
+ * reach (a `tsx` CHILD PROCESS, and the user's own `defineSeed` callback), both
3029
+ * of them through the `DATABASE_URL` this function returns.
3030
+ *
3031
+ * The probe also reads the connection's own `search_path`, so the pin EXTENDS
3032
+ * it rather than replacing it, exactly as `migrate` does. Without that, seeding
3033
+ * into a pinned schema stops resolving extension types and functions that the
3034
+ * unpinned connection could see.
3035
+ */
3036
+ async function seedConnectionString(config) {
3037
+ const schema = config.schema;
3038
+ if (schema === undefined || schema === '')
3039
+ return config.url;
3040
+ // Before the connection: an unpinnable NAME is not a missing schema.
3041
+ (0, migrate_js_1.assertPinnableSchema)(schema);
3042
+ const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
3043
+ const probe = new pg.Client({ connectionString: config.url });
3044
+ await probe.connect();
3045
+ let inherited;
3046
+ try {
3047
+ await (0, migrate_js_1.assertSchemaExists)(probe, schema);
3048
+ const shown = await probe.query('SHOW search_path');
3049
+ inherited = (0, connection_url_js_1.parseSearchPathValue)(shown.rows[0]?.search_path ?? '');
3050
+ }
3051
+ finally {
3052
+ await probe.end();
3053
+ }
3054
+ return (0, migrate_js_1.connectionStringForSchema)(config.url, schema, inherited);
3055
+ }
2888
3056
  async function runSeedPlan(plan, config) {
2889
3057
  const oldDatabaseUrl = process.env.DATABASE_URL;
2890
- if (config.url)
2891
- process.env.DATABASE_URL = config.url;
3058
+ // The seed connects through DATABASE_URL, whichever way it runs: `defineSeed`
3059
+ // reads it in the tsx child and in the in-process `import()`, and the `.sql`
3060
+ // path opens a client on it below. So the configured schema is applied HERE,
3061
+ // once, as the `options=-c search_path` connection parameter (never a `SET`),
3062
+ // and every seed kind inherits it. Without this the seed ran against the
3063
+ // role's default path while `push` and the generated client used
3064
+ // `config.schema`, so the two halves of one project wrote to two namespaces.
3065
+ const seedUrl = config.url ? await seedConnectionString(config) : undefined;
3066
+ if (seedUrl)
3067
+ process.env.DATABASE_URL = seedUrl;
2892
3068
  try {
2893
3069
  if (plan.kind === 'tsx') {
2894
3070
  if (!(0, loader_js_1.canResolveTsx)()) {
@@ -2907,7 +3083,7 @@ async function runSeedPlan(plan, config) {
2907
3083
  stdio: 'inherit',
2908
3084
  env: {
2909
3085
  ...process.env,
2910
- DATABASE_URL: config.url || process.env.DATABASE_URL,
3086
+ DATABASE_URL: seedUrl || process.env.DATABASE_URL,
2911
3087
  TURBINE_SEED_SENTINEL: sentinel,
2912
3088
  },
2913
3089
  });
@@ -2930,7 +3106,10 @@ async function runSeedPlan(plan, config) {
2930
3106
  await mod.default();
2931
3107
  return;
2932
3108
  }
2933
- const url = requireUrl(config);
3109
+ // `seedUrl` is already pinned and already schema-checked; `requireUrl` is
3110
+ // only reached when no url was configured at all, in which case there is no
3111
+ // schema to pin either.
3112
+ const url = seedUrl ?? requireUrl(config);
2934
3113
  const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
2935
3114
  const client = new pg.Client({ connectionString: url });
2936
3115
  await client.connect();
@@ -2990,6 +3169,7 @@ async function cmdStatus(_args, config) {
2990
3169
  (0, ui_js_1.newline)();
2991
3170
  const spinner = new ui_js_1.Spinner('Introspecting database').start();
2992
3171
  const schema = await (0, introspect_js_1.introspect)({
3172
+ keepColumnNames: config.keepColumnNames,
2993
3173
  connectionString: url,
2994
3174
  schema: config.schema,
2995
3175
  include: config.include.length ? config.include : undefined,
@@ -3135,6 +3315,7 @@ async function cmdDoctor(args, config) {
3135
3315
  }
3136
3316
  const spinner = jsonMode ? null : new ui_js_1.Spinner('Introspecting database').start();
3137
3317
  const schema = await (0, introspect_js_1.introspect)({
3318
+ keepColumnNames: config.keepColumnNames,
3138
3319
  connectionString: url,
3139
3320
  schema: config.schema,
3140
3321
  include: config.include.length ? config.include : undefined,
@@ -4141,6 +4322,7 @@ function showInitHelp() {
4141
4322
  console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't create the seed file or run the seed`);
4142
4323
  console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't push the schema to the database`);
4143
4324
  console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't generate the typed client`);
4325
+ console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema to write into config ${(0, ui_js_1.dim)('(default: public)')}`);
4144
4326
  (0, ui_js_1.newline)();
4145
4327
  }
4146
4328
  function showGenerateHelp() {
@@ -4248,8 +4430,9 @@ function showMigrateHelp() {
4248
4430
  console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a sanctioned migration pattern ${(0, ui_js_1.dim)('(create only, e.g. backfill)')}`);
4249
4431
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
4250
4432
  console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
4251
- console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation ${(0, ui_js_1.dim)('(migrate up only, advanced)')}`);
4433
+ console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums, REWRITING history ${(0, ui_js_1.dim)('(up / deploy / down)')}`);
4252
4434
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying migration statements without the interactive confirm`);
4435
+ console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema the migrations run in ${(0, ui_js_1.dim)('(default: public)')}`);
4253
4436
  console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
4254
4437
  (0, ui_js_1.newline)();
4255
4438
  console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
@@ -4276,7 +4459,8 @@ function showSeedHelp() {
4276
4459
  console.log(` ${(0, ui_js_1.dim)('TypeScript seeds run with')} ${(0, ui_js_1.cyan)('npx tsx')} ${(0, ui_js_1.dim)('and can export')} ${(0, ui_js_1.cyan)('defineSeed(fn)')}${(0, ui_js_1.dim)('.')}`);
4277
4460
  (0, ui_js_1.newline)();
4278
4461
  console.log(` ${(0, ui_js_1.bold)('Options:')}`);
4279
- console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
4462
+ console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
4463
+ console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema the seed writes into ${(0, ui_js_1.dim)('(default: public)')}`);
4280
4464
  (0, ui_js_1.newline)();
4281
4465
  }
4282
4466
  function showStatusHelp() {
@@ -4484,7 +4668,7 @@ function showHelp() {
4484
4668
  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)')}`);
4485
4669
  console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
4486
4670
  console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting ${(0, ui_js_1.dim)('(up/down/push)')}`);
4487
- 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)')}`);
4671
+ console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Re-baseline drifted checksums on ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} / ${(0, ui_js_1.cyan)('down')} ${(0, ui_js_1.dim)('(advanced)')}`);
4488
4672
  (0, ui_js_1.newline)();
4489
4673
  console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
4490
4674
  console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
@@ -4656,7 +4840,20 @@ function showVersion() {
4656
4840
  // Main
4657
4841
  // ---------------------------------------------------------------------------
4658
4842
  async function main() {
4659
- const args = parseArgs();
4843
+ let args;
4844
+ try {
4845
+ args = parseArgs();
4846
+ }
4847
+ catch (err) {
4848
+ // A last-resort net. Every flag refusal parseArgs makes goes through
4849
+ // `failArg` (banner, red line, hints) and exits there, so nothing is
4850
+ // expected here; anything that does arrive is a bug in the parser rather
4851
+ // than in the caller's command line, and printing it beats swallowing it.
4852
+ (0, ui_js_1.errorLine)();
4853
+ (0, ui_js_1.error)(err instanceof Error ? err.message : String(err));
4854
+ (0, ui_js_1.errorLine)();
4855
+ process.exit(1);
4856
+ }
4660
4857
  // Quick exits that don't need config
4661
4858
  if (args.command === 'help' || args.command === '--help' || args.command === '-h') {
4662
4859
  showHelp();
@@ -4786,7 +4983,20 @@ async function main() {
4786
4983
  }
4787
4984
  catch (err) {
4788
4985
  if (err instanceof Error) {
4789
- if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
4986
+ // TYPE first, message second. These branches sniff for substrings, and
4987
+ // `ValidationError` beat them by accident: a refused `--schema` value says
4988
+ // "used as a connection parameter", which contains "connection", so a bad
4989
+ // flag value was reported under "Could not connect to database" with three
4990
+ // firewall hints. A typed error already knows what it is.
4991
+ if (err instanceof errors_js_1.ValidationError) {
4992
+ (0, ui_js_1.errorLine)();
4993
+ (0, ui_js_1.error)((0, ui_js_1.redactUrl)(err.message));
4994
+ if (args.verbose && err.stack) {
4995
+ (0, ui_js_1.errorLine)();
4996
+ console.error((0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.stack)));
4997
+ }
4998
+ }
4999
+ else if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
4790
5000
  (0, ui_js_1.errorLine)();
4791
5001
  (0, ui_js_1.error)(`Could not connect to database`);
4792
5002
  (0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)((0, ui_js_1.redactUrl)(err.message))}`);
@@ -26,9 +26,32 @@ export interface McpTransport {
26
26
  */
27
27
  pool?: PgCompatPool;
28
28
  }
29
+ /**
30
+ * Why a session ended: an explicit {@link McpServerHandle.dispose} (a signal,
31
+ * stdin closing), or the server closing it ITSELF because the peer's framing
32
+ * was lost. `runMcpServer` exits non-zero on the latter so a supervisor
33
+ * restarts the server rather than assuming a clean end.
34
+ */
35
+ export type McpCloseReason = 'disposed' | 'framing-lost';
29
36
  export interface McpServerHandle {
30
37
  dispose(): Promise<void>;
38
+ /**
39
+ * Resolves once the session has ended and the pool is closed, by either
40
+ * path. After the unframed-buffer refusal the server used to detach its
41
+ * reader and then neither exit nor answer: a live process that would never
42
+ * speak again, which a supervisor cannot tell from a healthy idle one.
43
+ */
44
+ closed: Promise<McpCloseReason>;
31
45
  }
46
+ /**
47
+ * The most bytes the stdio reader will hold WITHOUT seeing a newline.
48
+ *
49
+ * 8 MiB, which is far above any real request: the largest thing a client sends
50
+ * here is a `compile_query` args object, and the tool schemas cap what can
51
+ * meaningfully be in one. It is a liveness bound, not a policy: see the check
52
+ * itself for why an over-long line ends the session instead of being truncated.
53
+ */
54
+ export declare const MAX_STDIO_BUFFER_BYTES: number;
32
55
  export declare function startMcpServer(options: McpServerOptions, transport?: McpTransport): McpServerHandle;
33
56
  /**
34
57
  * Every SHORTEST relation chain from `from` to `to`, in deterministic order.