@rebasepro/server-postgres 0.22.0 → 0.24.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 (123) hide show
  1. package/dist/{BranchService-CucnFcSE.js → BranchService-BRt78gfa.js} +35 -18
  2. package/dist/BranchService-BRt78gfa.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +116 -4
  4. package/dist/auth/services.d.ts +99 -22
  5. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  6. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  7. package/dist/backup/backup-cli.d.ts +22 -0
  8. package/dist/{backup-cli-DqBakiMO.js → backup-cli-DW5p9_zv.js} +17 -14
  9. package/dist/backup-cli-DW5p9_zv.js.map +1 -0
  10. package/dist/{backup-service-CaMOS76G.js → backup-service-EwcDVG-8.js} +7 -9
  11. package/dist/{backup-service-CaMOS76G.js.map → backup-service-EwcDVG-8.js.map} +1 -1
  12. package/dist/{cli-errors-Dka89exj.js → cli-errors-DsA-K9uP.js} +101 -1
  13. package/dist/cli-errors-DsA-K9uP.js.map +1 -0
  14. package/dist/cli-errors.d.ts +42 -0
  15. package/dist/cli-flags-BglvpjHv.js +138 -0
  16. package/dist/cli-flags-BglvpjHv.js.map +1 -0
  17. package/dist/cli-flags.d.ts +28 -0
  18. package/dist/cli-helpers.d.ts +18 -12
  19. package/dist/cli-scratch-database.d.ts +34 -0
  20. package/dist/cli.js +177 -287
  21. package/dist/cli.js.map +1 -1
  22. package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-1-LQD0yI.js} +44 -38
  23. package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
  24. package/dist/data-transformer.d.ts +0 -8
  25. package/dist/{doctor-CU9IogdL.js → doctor-C-sYWbmt.js} +228 -46
  26. package/dist/doctor-C-sYWbmt.js.map +1 -0
  27. package/dist/{ensure-collection-policies-Dzd-2S81.js → ensure-collection-policies-B1ureSIV.js} +73 -18
  28. package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
  29. package/dist/{ensure-collection-tables-CpAgy51F.js → ensure-collection-tables-kkkHk8oo.js} +124 -37
  30. package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
  31. package/dist/{ensure-tables-Cr5B4UmH.js → ensure-tables-BmI_tRxc.js} +10 -3
  32. package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
  33. package/dist/{generate-drizzle-schema-B537GIvz.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
  34. package/dist/{generate-drizzle-schema-B537GIvz.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
  35. package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
  36. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
  37. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
  38. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
  39. package/dist/generated-sql.d.ts +28 -0
  40. package/dist/index.es.js +4768 -1117
  41. package/dist/index.es.js.map +1 -1
  42. package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-kCETE8TY.js} +532 -39
  43. package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
  44. package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
  45. package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
  46. package/dist/{plan-schema-CboAIwLN.js → plan-schema-DU9exq6C.js} +291 -529
  47. package/dist/plan-schema-DU9exq6C.js.map +1 -0
  48. package/dist/{policy-drift-xJfy9xG7.js → policy-drift-B-J2hhm0.js} +3 -3
  49. package/dist/policy-drift-B-J2hhm0.js.map +1 -0
  50. package/dist/{generate-postgres-ddl-logic-D7imhYV8.js → render-ddl-Ds2t_d9V.js} +11 -149
  51. package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
  52. package/dist/{rls-bootstrap-sql-H3rCFi3F.js → rls-bootstrap-sql-_KNnjanK.js} +562 -35
  53. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
  54. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
  55. package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
  56. package/dist/schema/atlas-argv.d.ts +15 -0
  57. package/dist/schema/auth-schema.d.ts +170 -0
  58. package/dist/schema/classify-change.d.ts +29 -1
  59. package/dist/schema/column-plan-helpers.d.ts +51 -20
  60. package/dist/schema/destructive-sql.d.ts +71 -1
  61. package/dist/schema/doctor-cli.js +4 -4
  62. package/dist/schema/doctor.d.ts +34 -1
  63. package/dist/schema/ensure-collection-policies.d.ts +22 -0
  64. package/dist/schema/generate-drizzle-schema.js +1 -1
  65. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  66. package/dist/schema/generate-postgres-ddl.js +1 -1
  67. package/dist/schema/generate-schema-commit.d.ts +12 -0
  68. package/dist/schema/introspect-db-logic.d.ts +31 -0
  69. package/dist/schema/introspect-db-queries.d.ts +1 -1
  70. package/dist/schema/introspect-db-search.d.ts +22 -0
  71. package/dist/schema/introspect-db-storage.d.ts +83 -0
  72. package/dist/schema/introspect-db.js +15 -314
  73. package/dist/schema/introspect-db.js.map +1 -1
  74. package/dist/schema/plan/diff-plan.d.ts +4 -3
  75. package/dist/schema/plan/plan-schema.d.ts +26 -11
  76. package/dist/schema/plan/render-ddl.d.ts +6 -0
  77. package/dist/schema/plan/types.d.ts +43 -12
  78. package/dist/search-column-BM-GV6vH.js +442 -0
  79. package/dist/search-column-BM-GV6vH.js.map +1 -0
  80. package/dist/security/policy-drift.d.ts +1 -1
  81. package/dist/security/rls-enforcement.d.ts +7 -0
  82. package/dist/services/BranchService.d.ts +22 -1
  83. package/dist/services/FetchService.d.ts +102 -32
  84. package/dist/services/PersistService.d.ts +41 -5
  85. package/dist/services/RelationService.d.ts +29 -0
  86. package/dist/services/RelationWriteService.d.ts +6 -0
  87. package/dist/services/cdc/CdcListener.d.ts +15 -5
  88. package/dist/services/cdc/identity-columns.d.ts +19 -0
  89. package/dist/services/cdc/trigger-cdc.d.ts +45 -7
  90. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  91. package/dist/services/channel-history.d.ts +17 -1
  92. package/dist/services/collection-helpers.d.ts +21 -0
  93. package/dist/services/dataService.d.ts +3 -20
  94. package/dist/services/field-op-sql.d.ts +71 -0
  95. package/dist/services/junction-writes.d.ts +19 -3
  96. package/dist/services/pg-notify-listener.d.ts +95 -6
  97. package/dist/services/read-field-access.d.ts +19 -0
  98. package/dist/services/realtimeService.d.ts +232 -44
  99. package/dist/services/row-pipeline.d.ts +5 -0
  100. package/dist/services/socket-liveness.d.ts +45 -0
  101. package/dist/services/soft-delete.d.ts +12 -2
  102. package/dist/services/sql-script.d.ts +71 -0
  103. package/dist/services/write-depth.d.ts +16 -0
  104. package/dist/services/write-transaction-scope.d.ts +42 -0
  105. package/dist/utils/drizzle-conditions.d.ts +49 -4
  106. package/dist/utils/sql-redaction.d.ts +21 -0
  107. package/dist/websocket.d.ts +57 -18
  108. package/package.json +9 -9
  109. package/dist/BranchService-CucnFcSE.js.map +0 -1
  110. package/dist/backup-cli-DqBakiMO.js.map +0 -1
  111. package/dist/cli-errors-Dka89exj.js.map +0 -1
  112. package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
  113. package/dist/doctor-CU9IogdL.js.map +0 -1
  114. package/dist/ensure-collection-policies-Dzd-2S81.js.map +0 -1
  115. package/dist/ensure-collection-tables-CpAgy51F.js.map +0 -1
  116. package/dist/ensure-tables-Cr5B4UmH.js.map +0 -1
  117. package/dist/generate-drizzle-schema-logic-BcMl7VSy.js.map +0 -1
  118. package/dist/generate-postgres-ddl-logic-D7imhYV8.js.map +0 -1
  119. package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
  120. package/dist/plan-schema-CboAIwLN.js.map +0 -1
  121. package/dist/policy-drift-xJfy9xG7.js.map +0 -1
  122. package/dist/rls-bootstrap-sql-H3rCFi3F.js.map +0 -1
  123. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,105 +1,19 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { a as __toESM } from "./__vite-browser-external-BnuHet1e.js";
4
- import { a as formatForeignGeneratedColumnBanner, c as reportCommandFailure, i as diagnoseDbError, r as diagnoseAtlasFailure, t as checkDatabaseConnectivity } from "./cli-errors-Dka89exj.js";
4
+ import { a as formatForeignGeneratedColumnBanner, c as reportCommandFailure, i as diagnoseDbError, r as diagnoseAtlasFailure, t as checkDatabaseConnectivity } from "./cli-errors-DsA-K9uP.js";
5
5
  import { n as outError, r as outWarn, t as out } from "./cli-output-CNdMql-L.js";
6
- import { A as execa, C as loadCollectionsForCli, D as readTriggersDdl, E as readSearchDdl, O as readVectorDdl, S as getVectorExcludes, T as queryGeneratedColumnDependencies, _ as getDevDatabaseUrl, a as ExcludeIntrospectionError, b as getTableExcludes, c as applyVectorDdl, d as describeReinstallCommand, f as detectProjectPackageManager, g as ensureDevDatabaseExists, h as dropGeneratedColumns, j as forLibpq, k as resolveLocalBin, l as describeBuildScriptRemedy, m as dropDevDatabase, o as applySearchDdl, p as diagnoseMissingBin, r as dropLegacyAuthSchema, s as applyTriggersDdl, t as RLS_BOOTSTRAP_SQL, u as describeDevAddCommand, v as getForeignIndexExcludes, w as promptConfirm, x as getTriggerExcludes, y as getSearchExcludes } from "./rls-bootstrap-sql-H3rCFi3F.js";
6
+ import { A as resolveLocalBin, B as forLibpq, C as loadCollectionsForCli, D as readSearchDdl, E as queryGeneratedColumnDependencies, F as decidePushSafety, I as detectDestructiveStatements, L as extractPlanStatements, M as GENERATED_SQL_DIR, N as generatedSqlDir, O as readTriggersDdl, P as prepareGeneratedSqlDir, R as fromAlterTable, S as getVectorExcludes, T as queryColumnTypes, _ as getDevDatabaseUrl, a as ExcludeIntrospectionError, b as getTableExcludes, c as applyVectorDdl, d as describeReinstallCommand, f as detectProjectPackageManager, g as ensureDevDatabaseExists, h as dropGeneratedColumns, j as DESIRED_STATE_URL, k as readVectorDdl, l as describeBuildScriptRemedy, m as dropDevDatabase, o as applySearchDdl, p as diagnoseMissingBin, r as dropLegacyAuthSchema, s as applyTriggersDdl, t as RLS_BOOTSTRAP_SQL, u as describeDevAddCommand, v as getForeignIndexExcludes, w as promptConfirm, x as getTriggerExcludes, y as getSearchExcludes, z as execa } from "./rls-bootstrap-sql-_KNnjanK.js";
7
7
  import { t as chalk } from "./source-Br7L7GOI.js";
8
8
  import { t as require_arg } from "./arg-Dni7MzLB.js";
9
+ import { n as collectionsPathIn, r as parseDriverLine, t as assertKnownFlags } from "./cli-flags-BglvpjHv.js";
9
10
  import { logger } from "@rebasepro/server";
10
11
  import { formatRelativeTime } from "@rebasepro/utils";
11
12
  import fs from "fs";
12
13
  import path from "path";
13
14
  import { fileURLToPath } from "url";
14
- //#region src/schema/destructive-sql.ts
15
- var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
16
- /**
17
- * Pure helpers that classify an Atlas declarative-apply plan as destructive
18
- * or not, and decide what `rebase db push` should do about it.
19
- *
20
- * `db push` runs `atlas schema apply` to make the live database match the
21
- * generated `schema.sql`. Removing a collection field compiles to
22
- * `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
23
- * We first run the apply with `--dry-run` to obtain the planned SQL, scan it
24
- * here, and refuse to auto-approve anything destructive without an explicit
25
- * opt-in.
26
- *
27
- * Everything in this file is side-effect free so it can be unit-tested
28
- * without Atlas or a database.
29
- */
30
- /**
31
- * SQL fragments that destroy data or data-bearing objects. Matched
32
- * case-insensitively against each statement of the plan. `IF EXISTS` /
33
- * whitespace variations are tolerated by the regexes below.
34
- */
35
- var DESTRUCTIVE_PATTERNS = [
36
- {
37
- label: "DROP TABLE",
38
- re: /\bDROP\s+TABLE\b/i
39
- },
40
- {
41
- label: "DROP COLUMN",
42
- re: /\bDROP\s+COLUMN\b/i
43
- },
44
- {
45
- label: "DROP SCHEMA",
46
- re: /\bDROP\s+SCHEMA\b/i
47
- },
48
- {
49
- label: "DROP VIEW",
50
- re: /\bDROP\s+(MATERIALIZED\s+)?VIEW\b/i
51
- },
52
- {
53
- label: "DROP TYPE",
54
- re: /\bDROP\s+TYPE\b/i
55
- },
56
- {
57
- label: "TRUNCATE",
58
- re: /\bTRUNCATE\b/i
59
- }
60
- ];
61
- /**
62
- * Split a SQL script into individual statements, dropping blank lines and
63
- * `--` comment lines. Deliberately simple: Atlas emits one plain statement
64
- * per `;`, without string literals that contain semicolons in a schema DDL
65
- * plan, so a naive split is safe and keeps this dependency-free.
66
- */
67
- function splitSqlStatements(sql) {
68
- return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
69
- }
70
- /**
71
- * Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
72
- * the statements that would destroy data. An empty array means the plan is
73
- * safe to auto-approve.
74
- */
75
- function detectDestructiveStatements(planSql) {
76
- const found = [];
77
- for (const statement of splitSqlStatements(planSql)) for (const { label, re } of DESTRUCTIVE_PATTERNS) if (re.test(statement)) {
78
- found.push({
79
- statement,
80
- kind: label
81
- });
82
- break;
83
- }
84
- return found;
85
- }
86
- /**
87
- * Decide how `db push` should proceed given the plan's destructiveness and
88
- * the invocation context.
89
- *
90
- * - No destructive statements → `apply` (safe to auto-approve).
91
- * - Destructive + `--allow-destructive` → `apply` (operator opted in).
92
- * - Destructive + interactive TTY → `confirm` (prompt before applying).
93
- * - Destructive + non-interactive → `refuse` (never silently drop data in
94
- * CI / scripts / agents).
95
- */
96
- function decidePushSafety(opts) {
97
- if (opts.destructiveCount === 0) return "apply";
98
- if (opts.allowDestructive) return "apply";
99
- return opts.interactive ? "confirm" : "refuse";
100
- }
101
- //#endregion
102
15
  //#region src/schema/generated-column-conflicts.ts
16
+ var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
103
17
  /**
104
18
  * Which columns an Atlas plan cannot touch, because a generated column reads
105
19
  * them.
@@ -137,16 +51,6 @@ var ALTER_COLUMN_RE = new RegExp(String.raw`\bALTER\s+(?:COLUMN\s+)?(${IDENT$1})
137
51
  var DROP_COLUMN_RE$1 = new RegExp(String.raw`\bDROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(${IDENT$1})`, "gi");
138
52
  var unquote = (ident) => ident.startsWith("\"") ? ident.slice(1, -1) : ident;
139
53
  /**
140
- * Strip Atlas's plan rendering from a statement.
141
- *
142
- * What `schema apply --dry-run` prints is not a SQL file: each statement is
143
- * indented under a `-- modify "posts" table` heading and prefixed with `-> `,
144
- * with an `-- ok (12µs)` line after it. `splitSqlStatements` drops the comment
145
- * lines, and this drops the arrow — without it an anchored `^ALTER TABLE`
146
- * matches nothing at all, which is a silent no-op rather than an error.
147
- */
148
- var stripPlanMarker = (statement) => statement.replace(/^\s*->\s*/, "").trim();
149
- /**
150
54
  * Every column an Atlas plan retypes or drops, with the table it belongs to.
151
55
  *
152
56
  * One statement carries many clauses — Atlas emits `ALTER TABLE "public"."posts"
@@ -155,8 +59,8 @@ var stripPlanMarker = (statement) => statement.replace(/^\s*->\s*/, "").trim();
155
59
  */
156
60
  function parseColumnMutations(planSql) {
157
61
  const mutations = [];
158
- for (const raw of splitSqlStatements(planSql)) {
159
- const statement = stripPlanMarker(raw);
62
+ for (const raw of extractPlanStatements(planSql)) {
63
+ const statement = fromAlterTable(raw);
160
64
  const table = TABLE_RE.exec(statement);
161
65
  if (!table) continue;
162
66
  const [schema, name] = table[2] ? [unquote(table[1]), unquote(table[2])] : [void 0, unquote(table[1])];
@@ -650,6 +554,15 @@ function tidy(sql) {
650
554
  function acceptsExcludeFlag(domain, args) {
651
555
  return domain === "schema" && args.includes("apply");
652
556
  }
557
+ /**
558
+ * Does this Atlas invocation plan against the scratch database (`--dev-url`)?
559
+ *
560
+ * `schema apply` and `migrate diff` do; `migrate apply`, `hash` and `status`
561
+ * never read it, so `db migrate` has no reason to create one.
562
+ */
563
+ function takesDevUrl(domain, args) {
564
+ return domain === "schema" && args.includes("apply") || domain === "migrate" && args.includes("diff");
565
+ }
653
566
  /** Assemble the full argv, connection flags and all. */
654
567
  function buildAtlasArgs(invocation) {
655
568
  const { domain, args, url, devUrl, excludes = [] } = invocation;
@@ -667,6 +580,52 @@ function buildAtlasArgs(invocation) {
667
580
  if (acceptsExcludeFlag(domain, args)) for (const exclude of excludes) argv.push("--exclude", exclude);
668
581
  return argv;
669
582
  }
583
+ /**
584
+ * `atlas migrate apply`'s own arguments for `rebase db migrate`.
585
+ *
586
+ * `positionals` are what is left of the user's line once every flag the
587
+ * driver knows has been consumed — at most the optional amount Atlas takes. A
588
+ * flag reaching here is a flag Atlas rejects before doing any work.
589
+ */
590
+ function migrateApplyArgs(positionals, baseline) {
591
+ return [
592
+ "apply",
593
+ "--dir",
594
+ "file://drizzle/migrations",
595
+ ...baseline ? ["--baseline", baseline] : [],
596
+ ...positionals.filter((positional) => positional !== "migrate")
597
+ ];
598
+ }
599
+ //#endregion
600
+ //#region src/cli-scratch-database.ts
601
+ var ScratchDatabase = class {
602
+ ops;
603
+ created;
604
+ constructor(ops) {
605
+ this.ops = ops;
606
+ }
607
+ /** Make sure it exists before an Atlas call that plans against it. */
608
+ async ensure(databaseUrl, devDatabaseUrl) {
609
+ if (await this.ops.create(databaseUrl, devDatabaseUrl)) this.created = {
610
+ databaseUrl,
611
+ devDatabaseUrl
612
+ };
613
+ }
614
+ /**
615
+ * Run a command, then drop the scratch database if the command created it.
616
+ * A command that throws skips the drop; one that exits the process never
617
+ * returns here, which is the same thing.
618
+ */
619
+ async around(command) {
620
+ const result = await command();
621
+ const created = this.created;
622
+ if (created) {
623
+ this.created = void 0;
624
+ await this.ops.drop(created.databaseUrl, created.devDatabaseUrl);
625
+ }
626
+ return result;
627
+ }
628
+ };
670
629
  //#endregion
671
630
  //#region src/branch-argv.ts
672
631
  /**
@@ -728,140 +687,6 @@ var TAKES_A_VALUE = /* @__PURE__ */ new Set([
728
687
  "--database-url"
729
688
  ]);
730
689
  //#endregion
731
- //#region src/cli-flags.ts
732
- /**
733
- * The flags each driver command takes, in one place, and the check that
734
- * enforces them.
735
- *
736
- * Every parser in `cli.ts` runs `arg(..., { permissive: true })`, which does not
737
- * mean "be lenient" — it means **an undeclared flag becomes a positional**. So
738
- * `rebase db push --alow-destructive` did not fail: the typo landed in `_`, the
739
- * push ran with the destructive gate still closed, and the developer read the
740
- * refusal as Rebase ignoring the flag they had just typed. `rebase schema
741
- * generate --ouput src/schema.ts` was worse — it wrote the default path and
742
- * said nothing, so the next build compiled a file nobody had regenerated.
743
- *
744
- * The check has to live at the entry point rather than in those parsers, and
745
- * that is not a detail: `db push` and `db generate` re-enter `schemaCommand`
746
- * and `generatePostgresDdlCommand` with the *db* line, so a strict parser
747
- * inside either of them would reject `--allow-destructive` on a line where it
748
- * is correct. One validation, once, against the spec for the command the user
749
- * actually named; the inner parsers keep reading an already-validated line.
750
- *
751
- * Commands with their own argument handling — `db branch` (`unexpectedBranchArgs`),
752
- * `db backup`, `db restore`, `db backups` — are deliberately absent: they own
753
- * their spec, and a second list here would be one more thing to keep in step.
754
- * An absent entry is "not checked here", never "takes nothing".
755
- */
756
- /**
757
- * Flags that reach this process on a line it did not compose.
758
- *
759
- * `rebase db …` hands the driver the user's whole line: the CLI *reads*
760
- * `--database-url` and `--docker` to resolve the database and then relays them
761
- * verbatim, and `--debug` is what `bin/rebase.js` prints after every failure as
762
- * the thing to re-run with. Rejecting any of the three would make the driver
763
- * refuse commands the CLI documents.
764
- */
765
- var RELAYED_FLAGS = {
766
- "--database-url": String,
767
- "--docker": Boolean,
768
- "--debug": Boolean,
769
- "--help": Boolean,
770
- "-h": "--help"
771
- };
772
- /** Keyed by `"<domain> <subcommand>"`, the way the user types it. */
773
- var DRIVER_FLAG_SPECS = {
774
- "db push": {
775
- "--collections": String,
776
- "-c": "--collections",
777
- "--dry-run": Boolean,
778
- "--allow-destructive": Boolean,
779
- "--yes": Boolean,
780
- "-y": "--yes"
781
- },
782
- "db generate": {
783
- "--collections": String,
784
- "-c": "--collections"
785
- },
786
- "db migrate": { "--baseline": String },
787
- "schema generate": {
788
- "--collections": String,
789
- "-c": "--collections",
790
- "--output": String,
791
- "-o": "--output",
792
- "--out": "--output",
793
- "--watch": Boolean,
794
- "-w": "--watch"
795
- },
796
- "schema introspect": {
797
- "--output": String,
798
- "-o": "--output",
799
- "--out": "--output",
800
- "--collections": String,
801
- "-c": "--collections",
802
- "--force": Boolean,
803
- "-f": "--force",
804
- "--schema": String
805
- },
806
- "schema stale": {
807
- "--collections": String,
808
- "-c": "--collections",
809
- "--output": String,
810
- "-o": "--output",
811
- "--out": "--output",
812
- "--fix": Boolean
813
- }
814
- };
815
- /**
816
- * The `--collections` path the user typed, or `null` if they typed none.
817
- *
818
- * Read once, at the entry point, because every command that takes it re-enters
819
- * the generators with the same line and each of them resolved it again — which
820
- * is how "Collections path not found" came to be printed four times before the
821
- * real output, and how a path that does not exist got as far as *writing* an
822
- * empty schema.
823
- *
824
- * Permissive, and it has to be: {@link assertKnownFlags} has already judged the
825
- * line, and `db push` carries flags the `--collections` spec does not name.
826
- */
827
- function collectionsPathIn(args) {
828
- try {
829
- return (0, import_arg.default)({
830
- "--collections": String,
831
- "-c": "--collections"
832
- }, {
833
- argv: args.slice(2),
834
- permissive: true
835
- })["--collections"] ?? null;
836
- } catch {
837
- return null;
838
- }
839
- }
840
- /**
841
- * Reject a flag the named command does not take.
842
- *
843
- * `args` is the driver's whole line — `["db", "push", …]` — so the flags are
844
- * read from `args.slice(2)`, past the two command words.
845
- *
846
- * Throws rather than exiting: `runPluginCommand`'s caller already turns a
847
- * thrown error into one red line and exit 1, and a thrown error is what the
848
- * tests can see.
849
- */
850
- function assertKnownFlags(domain, subcommand, args) {
851
- if (!subcommand) return;
852
- const spec = DRIVER_FLAG_SPECS[`${domain} ${subcommand}`];
853
- if (!spec) return;
854
- try {
855
- (0, import_arg.default)({
856
- ...RELAYED_FLAGS,
857
- ...spec
858
- }, { argv: args.slice(2) });
859
- } catch (err) {
860
- if (err instanceof Error && err.code === "ARG_UNKNOWN_OPTION") throw new Error(`${err.message} — run \`rebase ${domain} --help\` for the options \`${domain} ${subcommand}\` takes.`);
861
- throw err;
862
- }
863
- }
864
- //#endregion
865
690
  //#region src/cli-collections-path.ts
866
691
  /**
867
692
  * `--collections <dir>`, checked once, before anything is written.
@@ -870,8 +695,8 @@ function assertKnownFlags(domain, subcommand, args) {
870
695
  * `schemaCommand`, `generatePostgresDdlCommand` and the Atlas argv assembly
871
696
  * each re-enter the loader with the same line — and then both generators wrote
872
697
  * an *empty* schema: `drizzle/schema.sql` truncated to `CREATE SCHEMA IF NOT
873
- * EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both are committed
874
- * artifacts. The push that followed planned a `DROP TABLE` for every table in
698
+ * EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both were committed
699
+ * artifacts then. The push that followed planned a `DROP TABLE` for every table in
875
700
  * the database, and the only thing between a typo and that was the destructive
876
701
  * gate.
877
702
  *
@@ -1069,7 +894,7 @@ async function runPluginCommand(args) {
1069
894
  const subcommand = args[1];
1070
895
  assertKnownFlags(domain, subcommand, args);
1071
896
  assertCollectionsPathExists(collectionsPathIn(args));
1072
- if (domain === "db") await dbCommand(subcommand, args);
897
+ if (domain === "db") await scratchDatabase.around(() => dbCommand(subcommand, args));
1073
898
  else if (domain === "schema") await schemaCommand(subcommand, args);
1074
899
  else if (domain === "doctor") await doctorPluginCommand(args);
1075
900
  else {
@@ -1146,6 +971,20 @@ async function stripCarvedOutFromNewestMigration(collectionsPath) {
1146
971
  ], collectionsPath);
1147
972
  return !result.empty;
1148
973
  }
974
+ /**
975
+ * The flags `rebase db` reads for the subcommands it parses itself. Hoisted
976
+ * and named `…FLAGS` so the documentation verifier, which reads specs out of
977
+ * this source, sees them.
978
+ */
979
+ var DB_FLAGS = {
980
+ "--collections": String,
981
+ "--allow-destructive": Boolean,
982
+ "--dry-run": Boolean,
983
+ "--yes": Boolean,
984
+ "--baseline": String,
985
+ "-c": "--collections",
986
+ "-y": "--yes"
987
+ };
1149
988
  async function dbCommand(subcommand, rawArgs) {
1150
989
  const VALID_ACTIONS = [
1151
990
  "push",
@@ -1172,25 +1011,14 @@ async function dbCommand(subcommand, rawArgs) {
1172
1011
  return;
1173
1012
  }
1174
1013
  if (subcommand === "backup" || subcommand === "restore" || subcommand === "backups") {
1175
- const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-DqBakiMO.js");
1014
+ const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-DW5p9_zv.js");
1176
1015
  if (subcommand === "backup" && backupActionOf(rawArgs) === "list") await backupsCommand(rawArgs);
1177
1016
  else if (subcommand === "backup") await backupCommand(rawArgs);
1178
1017
  else if (subcommand === "restore") await restoreCommand(rawArgs);
1179
1018
  else await backupsCommand(rawArgs);
1180
1019
  return;
1181
1020
  }
1182
- const argsList = (0, import_arg.default)({
1183
- "--collections": String,
1184
- "--allow-destructive": Boolean,
1185
- "--dry-run": Boolean,
1186
- "--yes": Boolean,
1187
- "--baseline": String,
1188
- "-c": "--collections",
1189
- "-y": "--yes"
1190
- }, {
1191
- argv: rawArgs.slice(2),
1192
- permissive: true
1193
- });
1021
+ const argsList = parseDriverLine(DB_FLAGS, rawArgs);
1194
1022
  const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1195
1023
  if (subcommand === "generate") {
1196
1024
  out("");
@@ -1210,7 +1038,7 @@ async function dbCommand(subcommand, rawArgs) {
1210
1038
  "--dir",
1211
1039
  "file://drizzle/migrations",
1212
1040
  "--to",
1213
- "file://drizzle/schema.sql"
1041
+ DESIRED_STATE_URL
1214
1042
  ], collectionsPath);
1215
1043
  let wroteNewMigration = listMigrationFiles().length > migrationsBefore.length;
1216
1044
  if (wroteNewMigration) wroteNewMigration = await stripCarvedOutFromNewestMigration(collectionsPath);
@@ -1229,7 +1057,7 @@ async function dbCommand(subcommand, rawArgs) {
1229
1057
  }
1230
1058
  const searchContent = readSearchDdl();
1231
1059
  if (searchContent) {
1232
- const { searchColumnDependencies } = await import("./generate-postgres-ddl-logic-D7imhYV8.js").then((n) => n.o);
1060
+ const { searchColumnDependencies } = await import("./generate-postgres-ddl-logic-BJsLaVNX.js").then((n) => n.o);
1233
1061
  const migrationConflicts = findGeneratedColumnConflicts(parseColumnMutations(migrationContent), searchColumnDependencies(await loadCollectionsForCli(collectionsPath)));
1234
1062
  const preamble = migrationDropPreamble(migrationConflicts);
1235
1063
  if (preamble) {
@@ -1245,7 +1073,7 @@ async function dbCommand(subcommand, rawArgs) {
1245
1073
  out(chalk.gray(" ✓ Appended `updated_at` triggers to the migration"));
1246
1074
  }
1247
1075
  fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
1248
- const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
1076
+ const policiesFile = path.join(generatedSqlDir(), "policies.sql");
1249
1077
  if (fs.existsSync(policiesFile)) {
1250
1078
  const policiesContent = fs.readFileSync(policiesFile, "utf-8");
1251
1079
  fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
@@ -1263,7 +1091,10 @@ async function dbCommand(subcommand, rawArgs) {
1263
1091
  } catch (err) {
1264
1092
  outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
1265
1093
  }
1266
- if (!wroteNewMigration) out(chalk.gray(" ℹ No migration written — nothing in this change is visible to Atlas.\n Search, vector, trigger and RLS DDL are applied by `rebase db push`, and at\n boot by the schema ensure. For a migration-only deployment, add\n drizzle/search.sql, drizzle/vector.sql, drizzle/triggers.sql and\n drizzle/policies.sql to a migration by hand."));
1094
+ if (!wroteNewMigration) out(chalk.gray(` ℹ No migration written — nothing in this change is visible to Atlas.
1095
+ Search, vector, trigger and RLS DDL are applied by \`rebase db push\`, and at
1096
+ boot by the schema ensure. For a migration-only deployment, add
1097
+ ${GENERATED_SQL_DIR}/search.sql, vector.sql, triggers.sql and\n policies.sql to a migration by hand.`));
1267
1098
  out("");
1268
1099
  out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
1269
1100
  out("");
@@ -1285,17 +1116,20 @@ async function dbCommand(subcommand, rawArgs) {
1285
1116
  const plan = await runAtlas("schema", [
1286
1117
  "apply",
1287
1118
  "--to",
1288
- "file://drizzle/schema.sql",
1119
+ DESIRED_STATE_URL,
1289
1120
  "--dry-run"
1290
1121
  ], collectionsPath, { captureStdout: true });
1291
- const destructive = detectDestructiveStatements(plan);
1122
+ const destructive = detectDestructiveStatements(plan, databaseUrl ? await queryColumnTypes(databaseUrl) : []);
1123
+ const unreadable = destructive.some((d) => d.kind === "UNREADABLE PLAN");
1124
+ const planStatements = extractPlanStatements(plan);
1125
+ const planListing = unreadable ? plan.trim() : planStatements.map((statement) => `${statement};`).join("\n");
1292
1126
  const { managed: rebuildable, foreign: unowned } = partitionByOwnership(databaseUrl ? findGeneratedColumnConflicts(parseColumnMutations(plan), await queryGeneratedColumnDependencies(databaseUrl)) : [], collectionsPath ? await getSearchExcludes(collectionsPath) : []);
1293
1127
  if (dryRun) {
1294
1128
  out(chalk.gray(" Step 3/3: --dry-run — nothing was applied."));
1295
1129
  out("");
1296
- if (plan.trim()) {
1130
+ if (planStatements.length > 0 || unreadable) {
1297
1131
  out(chalk.bold(" Planned changes:"));
1298
- out(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
1132
+ out(chalk.gray(planListing.split("\n").map((l) => ` ${l}`).join("\n")));
1299
1133
  } else out(chalk.green(" ✓ No changes: the database already matches these collections."));
1300
1134
  out("");
1301
1135
  if (rebuildable.length > 0) {
@@ -1308,7 +1142,7 @@ async function dbCommand(subcommand, rawArgs) {
1308
1142
  if (unowned.length > 0) outWarn(formatForeignGeneratedColumnBanner(unowned));
1309
1143
  if (destructive.length > 0) {
1310
1144
  outWarn(chalk.yellow(` ⚠️ ${destructive.length} of those DESTROY data:`));
1311
- for (const d of destructive) outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1145
+ for (const d of destructive) outWarn(chalk.red(` ${d.kind}${d.detail ? ` (${d.detail})` : ""}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1312
1146
  outWarn("");
1313
1147
  outWarn(chalk.yellow(" Applying them needs `rebase db push --allow-destructive`. Back up first: rebase db backup"));
1314
1148
  outWarn("");
@@ -1330,10 +1164,10 @@ async function dbCommand(subcommand, rawArgs) {
1330
1164
  if (destructive.length > 0) {
1331
1165
  outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
1332
1166
  outWarn("");
1333
- for (const d of destructive) outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1167
+ for (const d of destructive) outWarn(chalk.red(` ${d.kind}${d.detail ? ` (${d.detail})` : ""}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1334
1168
  outWarn("");
1335
1169
  outWarn(chalk.yellow(" Full planned changes:"));
1336
- outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
1170
+ outWarn(chalk.gray(planListing.split("\n").map((l) => ` ${l}`).join("\n")));
1337
1171
  outWarn("");
1338
1172
  if (decision === "refuse") {
1339
1173
  outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
@@ -1355,9 +1189,9 @@ async function dbCommand(subcommand, rawArgs) {
1355
1189
  await runAtlas("schema", [
1356
1190
  "apply",
1357
1191
  "--to",
1358
- "file://drizzle/schema.sql",
1192
+ DESIRED_STATE_URL,
1359
1193
  "--auto-approve"
1360
- ], collectionsPath);
1194
+ ], collectionsPath, { throwOnFailure: true });
1361
1195
  } catch (err) {
1362
1196
  if (rebuildable.length > 0 && databaseUrl) {
1363
1197
  out(chalk.gray(" The apply failed — rebuilding the generated columns it needed out of the way."));
@@ -1367,6 +1201,7 @@ async function dbCommand(subcommand, rawArgs) {
1367
1201
  }
1368
1202
  out("");
1369
1203
  if (databaseUrl) {
1204
+ await lockManagedTables(databaseUrl, collectionsPath);
1370
1205
  await ensureAuthTables(databaseUrl, collectionsPath);
1371
1206
  await applyVectorDdl(databaseUrl);
1372
1207
  await applySearchDdl(databaseUrl);
@@ -1375,24 +1210,16 @@ async function dbCommand(subcommand, rawArgs) {
1375
1210
  await reconcilePolicies(databaseUrl, collectionsPath);
1376
1211
  await ensureRlsUserRole(databaseUrl);
1377
1212
  await retireLegacyAuthSchema(databaseUrl);
1378
- if (scratchDatabaseCreatedHere) await dropDevDatabase(databaseUrl, getDevDatabaseUrl(databaseUrl));
1379
1213
  } else outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
1380
1214
  } else if (subcommand === "migrate") {
1381
1215
  const databaseUrl = process.env.DATABASE_URL;
1382
1216
  if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
1383
- const extraArgs = argsList._.filter((arg) => arg !== "migrate");
1384
1217
  const baseline = argsList["--baseline"];
1385
1218
  if (baseline) {
1386
1219
  out(chalk.gray(` Recording ${baseline} as already applied, then migrating from the next one.`));
1387
1220
  out("");
1388
1221
  }
1389
- await runAtlas("migrate", [
1390
- "apply",
1391
- "--dir",
1392
- "file://drizzle/migrations",
1393
- ...baseline ? ["--baseline", baseline] : [],
1394
- ...extraArgs
1395
- ], collectionsPath);
1222
+ await runAtlas("migrate", migrateApplyArgs(argsList._, baseline), collectionsPath);
1396
1223
  if (databaseUrl) {
1397
1224
  await ensureRlsUserRole(databaseUrl);
1398
1225
  await retireLegacyAuthSchema(databaseUrl);
@@ -1418,6 +1245,58 @@ async function ensureAuthSchemaAndFunctions(databaseUrl) {
1418
1245
  }
1419
1246
  }
1420
1247
  /**
1248
+ * Switch row-level security on for every table the collections declare, right
1249
+ * after Atlas has applied the schema.
1250
+ *
1251
+ * Atlas cannot carry `ENABLE ROW LEVEL SECURITY` in `schema.sql`: its free tier
1252
+ * neither plans it nor reads it back, so a table it creates is born with RLS
1253
+ * off — and `rebase_user`, the role every authenticated request runs as, holds
1254
+ * DML on it from that moment through the schema's default privileges. RLS used
1255
+ * to arrive with the policies, the last step of the push; a refusal from the
1256
+ * vector, search or trigger step in between left the new table readable and
1257
+ * writable by every caller, with no policy, until a later push or boot fixed
1258
+ * it — and a deployment with `REBASE_MIGRATE_ON_BOOT=none` never did. Locked
1259
+ * first, a failure later leaves a table that denies, not one that is open.
1260
+ *
1261
+ * Fatal when a table cannot be locked, and before the steps that follow: the
1262
+ * role provisioning at the end re-grants on every table in the schema, which
1263
+ * would undo the revoke that is the fallback here.
1264
+ */
1265
+ async function lockManagedTables(databaseUrl, collectionsPath) {
1266
+ let unsecured;
1267
+ let userRole;
1268
+ try {
1269
+ const { lockCollectionTables } = await import("./ensure-collection-policies-B1ureSIV.js");
1270
+ const { loadCollections } = await import("./doctor-C-sYWbmt.js").then((n) => n.t);
1271
+ userRole = (await import("./rls-enforcement-CfXOJJaW.js").then((n) => n.o)).REBASE_USER_ROLE;
1272
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1273
+ const { Client } = await import("pg");
1274
+ const client = new Client({ connectionString: databaseUrl });
1275
+ await client.connect();
1276
+ try {
1277
+ ({unsecured} = await lockCollectionTables({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
1278
+ } finally {
1279
+ await client.end();
1280
+ }
1281
+ } catch (err) {
1282
+ outError(chalk.red(`\n ✗ The schema was applied, but row-level security could not be switched on for its tables: ${err instanceof Error ? err.message : String(err)}`));
1283
+ outError(chalk.gray(" A table this push created may be readable and writable by every authenticated request\n until it is. Re-run `rebase db push` — it locks every collection table before anything else."));
1284
+ process.exit(1);
1285
+ }
1286
+ if (unsecured.length === 0) return;
1287
+ outError(chalk.red("\n ✗ The schema was applied, but row-level security could not be switched on for:"));
1288
+ for (const u of unsecured) {
1289
+ outError(chalk.red(` ${u.table}: ${u.error}`));
1290
+ outError(chalk.gray(u.grantWithdrawn ? ` Its privileges were taken back from ${userRole}, so it is unreachable rather than unprotected.` : ` ${userRole} still holds its privileges: every authenticated request can read and write every row.`));
1291
+ }
1292
+ outError(chalk.gray(" Nothing after the schema apply ran. Fix the cause — the connection role must own these\n tables, or be able to ALTER them — and run `rebase db push` again, or as their owner:"));
1293
+ for (const u of unsecured) {
1294
+ const [schema, ...table] = u.table.split(".");
1295
+ outError(chalk.bold(` ALTER TABLE "${schema}"."${table.join(".")}" ENABLE ROW LEVEL SECURITY;`));
1296
+ }
1297
+ process.exit(1);
1298
+ }
1299
+ /**
1421
1300
  * Create the framework auth tables (e.g. `rebase.users`) that the generated RLS
1422
1301
  * policies reference, before `applyPolicies` runs. Mirrors what the server does
1423
1302
  * at boot (`PostgresBootstrapper.initializeAuth` → `ensureAuthTablesExist`).
@@ -1431,8 +1310,8 @@ async function ensureAuthSchemaAndFunctions(databaseUrl) {
1431
1310
  async function ensureAuthTables(databaseUrl, collectionsPath) {
1432
1311
  try {
1433
1312
  const { drizzle } = await import("drizzle-orm/node-postgres");
1434
- const { ensureAuthTablesExist } = await import("./ensure-tables-Cr5B4UmH.js").then((n) => n.n);
1435
- const { loadCollections } = await import("./doctor-CU9IogdL.js").then((n) => n.t);
1313
+ const { ensureAuthTablesExist } = await import("./ensure-tables-BmI_tRxc.js").then((n) => n.n);
1314
+ const { loadCollections } = await import("./doctor-C-sYWbmt.js").then((n) => n.t);
1436
1315
  const { Client } = await import("pg");
1437
1316
  const authCollection = (await loadCollections(path.resolve(process.cwd(), collectionsPath))).find((c) => {
1438
1317
  const a = c.auth;
@@ -1459,7 +1338,7 @@ async function ensureRlsUserRole(databaseUrl) {
1459
1338
  const schemas = ["public", "rebase"];
1460
1339
  let rls;
1461
1340
  try {
1462
- rls = await import("./rls-enforcement-C6Xk0lA6.js").then((n) => n.a);
1341
+ rls = await import("./rls-enforcement-CfXOJJaW.js").then((n) => n.o);
1463
1342
  } catch (err) {
1464
1343
  outError(chalk.red(`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`));
1465
1344
  return;
@@ -1508,7 +1387,7 @@ async function retireLegacyAuthSchema(databaseUrl) {
1508
1387
  }
1509
1388
  async function applyPolicies(databaseUrl) {
1510
1389
  try {
1511
- const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
1390
+ const policiesPath = path.join(generatedSqlDir(), "policies.sql");
1512
1391
  if (!fs.existsSync(policiesPath)) return;
1513
1392
  out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
1514
1393
  out("");
@@ -1544,8 +1423,8 @@ async function applyPolicies(databaseUrl) {
1544
1423
  */
1545
1424
  async function reconcilePolicies(databaseUrl, collectionsPath) {
1546
1425
  try {
1547
- const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-xJfy9xG7.js").then((n) => n.a);
1548
- const { loadCollections } = await import("./doctor-CU9IogdL.js").then((n) => n.t);
1426
+ const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-B-J2hhm0.js").then((n) => n.a);
1427
+ const { loadCollections } = await import("./doctor-C-sYWbmt.js").then((n) => n.t);
1549
1428
  const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1550
1429
  const { Client } = await import("pg");
1551
1430
  const client = new Client({ connectionString: databaseUrl });
@@ -1627,7 +1506,7 @@ async function branchCommand(rawArgs) {
1627
1506
  process.exit(1);
1628
1507
  }
1629
1508
  const { DatabasePoolManager } = await import("./databasePoolManager-Bj5FbeAs.js").then((n) => n.n);
1630
- const { BranchService } = await import("./BranchService-CucnFcSE.js").then((n) => n.n);
1509
+ const { BranchService } = await import("./BranchService-BRt78gfa.js").then((n) => n.n);
1631
1510
  const { drizzle } = await import("drizzle-orm/node-postgres");
1632
1511
  const { Pool } = await import("pg");
1633
1512
  const pool = new Pool({
@@ -1857,8 +1736,14 @@ function formatBytes(bytes) {
1857
1736
  function timeAgo(date) {
1858
1737
  return formatRelativeTime(date, { maxMs: Number.POSITIVE_INFINITY }) ?? "unknown";
1859
1738
  }
1860
- /** Set when this process created the Atlas scratch database; see `ensureDevDatabaseExists`. */
1861
- var scratchDatabaseCreatedHere = false;
1739
+ /**
1740
+ * The Atlas scratch database this process created, dropped once the db
1741
+ * command that needed it succeeds — see `ScratchDatabase`.
1742
+ */
1743
+ var scratchDatabase = new ScratchDatabase({
1744
+ create: ensureDevDatabaseExists,
1745
+ drop: dropDevDatabase
1746
+ });
1862
1747
  async function runAtlas(domain, args, collectionsPath, opts = {}) {
1863
1748
  const atlasBin = resolveLocalBin("atlas");
1864
1749
  if (!atlasBin) {
@@ -1911,7 +1796,7 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1911
1796
  }
1912
1797
  await checkDatabaseConnectivity(databaseUrl);
1913
1798
  const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
1914
- if (await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl)) scratchDatabaseCreatedHere = true;
1799
+ if (takesDevUrl(domain, args)) await scratchDatabase.ensure(databaseUrl, devDatabaseUrl);
1915
1800
  const atlasUrl = forLibpq(databaseUrl);
1916
1801
  const atlasDevUrl = forLibpq(devDatabaseUrl);
1917
1802
  const excludes = [];
@@ -1980,6 +1865,7 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1980
1865
  latestMigrationVersion: latestMigrationVersion()
1981
1866
  }) ?? diagnoseDbError({ message: stderrText }, databaseUrl);
1982
1867
  if (hint) outError(hint);
1868
+ if (opts.throwOnFailure) throw Object.assign(/* @__PURE__ */ new Error(`atlas ${domain} ${args.join(" ")} failed`), { alreadyReported: true });
1983
1869
  process.exit(1);
1984
1870
  }
1985
1871
  if (opts.captureStdout) return stdoutText.trim().length > 0 ? stdoutText : stderrText;
@@ -2001,7 +1887,11 @@ async function generatePostgresDdlCommand(rawArgs) {
2001
1887
  process.exit(1);
2002
1888
  }
2003
1889
  const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
2004
- const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
1890
+ const outputPath = argsList["--output"] || path.join(".rebase/sql", "schema.sql");
1891
+ if (!argsList["--output"]) {
1892
+ const retired = prepareGeneratedSqlDir();
1893
+ if (retired.length > 0) out(chalk.gray(` Generated SQL is written to ${GENERATED_SQL_DIR}/ now, which is not committed. Removed the old copies — commit the deletion:\n ${retired.join(", ")}`));
1894
+ }
2005
1895
  const cmdParts = [
2006
1896
  ddl.bin,
2007
1897
  ddl.script,
@@ -2053,9 +1943,9 @@ async function schemaStaleCommand(rawArgs) {
2053
1943
  const schemaFile = path.resolve(process.cwd(), outputPath);
2054
1944
  const fix = Boolean(argsList["--fix"]);
2055
1945
  const generatedExists = fs.existsSync(schemaFile);
2056
- const { loadCollections } = await import("./doctor-CU9IogdL.js").then((n) => n.t);
1946
+ const { loadCollections } = await import("./doctor-C-sYWbmt.js").then((n) => n.t);
2057
1947
  const { findLegacyForeignKeyNames, compareGeneratedDeclarations, staleVerdict } = await import("./generated-schema-staleness-Di9c4ZxN.js").then((n) => n.r);
2058
- const { generateSchema } = await import("./generate-drizzle-schema-logic-BcMl7VSy.js").then((n) => n.n);
1948
+ const { generateSchema } = await import("./generate-drizzle-schema-logic-sSFDp6LR.js").then((n) => n.n);
2059
1949
  const { relationalCollections } = await import("@rebasepro/common");
2060
1950
  let stale = [];
2061
1951
  let differences = [];