@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g19596ef

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 (208) hide show
  1. package/README.md +16 -0
  2. package/dist/{BranchService-DGPL6G_C.js → BranchService-UgKa416x.js} +47 -19
  3. package/dist/BranchService-UgKa416x.js.map +1 -0
  4. package/dist/PostgresBackendDriver.d.ts +187 -13
  5. package/dist/PostgresBootstrapper.d.ts +101 -2
  6. package/dist/auth/ensure-tables.d.ts +12 -0
  7. package/dist/auth/services.d.ts +78 -14
  8. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  9. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  10. package/dist/backup/backup-cli.d.ts +75 -2
  11. package/dist/backup/backup-service.d.ts +2 -2
  12. package/dist/backup/pg-binary.d.ts +5 -0
  13. package/dist/{backup-cli-24v4OlSp.js → backup-cli-yiKd1GsC.js} +168 -75
  14. package/dist/backup-cli-yiKd1GsC.js.map +1 -0
  15. package/dist/{backup-service-HQ9GC4tN.js → backup-service-AZWc2wvI.js} +104 -35
  16. package/dist/backup-service-AZWc2wvI.js.map +1 -0
  17. package/dist/boot-ddl-B9ZmBJ75.js +105 -0
  18. package/dist/boot-ddl-B9ZmBJ75.js.map +1 -0
  19. package/dist/cli-console-DGee3Zl7.js +225 -0
  20. package/dist/cli-console-DGee3Zl7.js.map +1 -0
  21. package/dist/cli-console.d.ts +39 -0
  22. package/dist/cli-errors-oVUuD8k0.js +2180 -0
  23. package/dist/cli-errors-oVUuD8k0.js.map +1 -0
  24. package/dist/cli-errors.d.ts +133 -9
  25. package/dist/{cli-flags-BglvpjHv.js → cli-flags-DnPHXjRH.js} +56 -6
  26. package/dist/cli-flags-DnPHXjRH.js.map +1 -0
  27. package/dist/cli-flags.d.ts +15 -0
  28. package/dist/cli-helpers.d.ts +88 -34
  29. package/dist/cli-output.d.ts +16 -28
  30. package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
  31. package/dist/cli-prompt-egtNKkZl.js.map +1 -0
  32. package/dist/cli-prompt.d.ts +19 -0
  33. package/dist/cli-push.d.ts +87 -0
  34. package/dist/cli-scratch-database.d.ts +33 -8
  35. package/dist/cli.js +826 -230
  36. package/dist/cli.js.map +1 -1
  37. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-D_V4oaZh.js} +43 -27
  38. package/dist/column-plan-helpers-D_V4oaZh.js.map +1 -0
  39. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C938nR3E.js} +25 -113
  40. package/dist/connection-C938nR3E.js.map +1 -0
  41. package/dist/connection.d.ts +13 -2
  42. package/dist/data-transformer.d.ts +0 -8
  43. package/dist/databasePoolManager-DZgnBOgj.js +119 -0
  44. package/dist/databasePoolManager-DZgnBOgj.js.map +1 -0
  45. package/dist/deferred-index-build-BqmEHGaD.js +1051 -0
  46. package/dist/deferred-index-build-BqmEHGaD.js.map +1 -0
  47. package/dist/{doctor-Cb2thZ8s.js → doctor-CUAAOFZm.js} +281 -89
  48. package/dist/doctor-CUAAOFZm.js.map +1 -0
  49. package/dist/ensure-collection-policies-Bb51V2OV.js +541 -0
  50. package/dist/ensure-collection-policies-Bb51V2OV.js.map +1 -0
  51. package/dist/ensure-collection-tables-egjGBjhv.js +532 -0
  52. package/dist/ensure-collection-tables-egjGBjhv.js.map +1 -0
  53. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-DMPWhJ6R.js} +180 -77
  54. package/dist/ensure-tables-DMPWhJ6R.js.map +1 -0
  55. package/dist/enum-labels-kept-Msq5gMcb.js +56 -0
  56. package/dist/enum-labels-kept-Msq5gMcb.js.map +1 -0
  57. package/dist/generate-drizzle-schema-DrA-YgXv.js +57 -0
  58. package/dist/generate-drizzle-schema-DrA-YgXv.js.map +1 -0
  59. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +29 -11
  60. package/dist/generate-drizzle-schema-logic-BaT0H7Gq.js.map +1 -0
  61. package/dist/generate-files-OoRJLEdk.js +199 -0
  62. package/dist/generate-files-OoRJLEdk.js.map +1 -0
  63. package/dist/generate-postgres-ddl-logic-B4k9djAV.js +152 -0
  64. package/dist/generate-postgres-ddl-logic-B4k9djAV.js.map +1 -0
  65. package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-BWN_PsMi.js} +2 -2
  66. package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-BWN_PsMi.js.map} +1 -1
  67. package/dist/generated-sql.d.ts +34 -0
  68. package/dist/history/HistoryService.d.ts +64 -5
  69. package/dist/history/ensure-history-table.d.ts +63 -4
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.es.js +139 -16738
  72. package/dist/index.es.js.map +1 -1
  73. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-BCc1Jym2.js} +654 -353
  74. package/dist/introspect-db-logic-BCc1Jym2.js.map +1 -0
  75. package/dist/introspect-db-queries-BhhJg0b8.js +364 -0
  76. package/dist/introspect-db-queries-BhhJg0b8.js.map +1 -0
  77. package/dist/open-rebase-CAjWpPjs.js +20895 -0
  78. package/dist/open-rebase-CAjWpPjs.js.map +1 -0
  79. package/dist/open-rebase.d.ts +94 -0
  80. package/dist/pg-error-utils-Bs_Ue4wn.js +395 -0
  81. package/dist/pg-error-utils-Bs_Ue4wn.js.map +1 -0
  82. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-CsNgK0qi.js} +279 -534
  83. package/dist/plan-schema-CsNgK0qi.js.map +1 -0
  84. package/dist/{policy-drift-DljYdrpW.js → policy-drift-BKC50y2p.js} +33 -9
  85. package/dist/policy-drift-BKC50y2p.js.map +1 -0
  86. package/dist/push-index-prebuild-C_qhyoJa.js +45 -0
  87. package/dist/push-index-prebuild-C_qhyoJa.js.map +1 -0
  88. package/dist/push-legacy-renames-5g9h3HOa.js +34 -0
  89. package/dist/push-legacy-renames-5g9h3HOa.js.map +1 -0
  90. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CrfEe__B.js} +24 -157
  91. package/dist/render-ddl-CrfEe__B.js.map +1 -0
  92. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-Bd-hJk08.js} +125 -1101
  93. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
  94. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-D7P4-ZYX.js} +138 -18
  95. package/dist/rls-enforcement-D7P4-ZYX.js.map +1 -0
  96. package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
  97. package/dist/schema/atlas-exclude.d.ts +42 -0
  98. package/dist/schema/auth-schema.d.ts +172 -2
  99. package/dist/schema/boot-ddl.d.ts +47 -0
  100. package/dist/schema/carved-out-migration.d.ts +5 -4
  101. package/dist/schema/classify-change.d.ts +29 -1
  102. package/dist/schema/collection-index.d.ts +45 -0
  103. package/dist/schema/column-plan-helpers.d.ts +38 -21
  104. package/dist/schema/deferred-index-build.d.ts +131 -0
  105. package/dist/schema/destructive-sql.d.ts +50 -2
  106. package/dist/schema/doctor-cli.js +110 -6
  107. package/dist/schema/doctor-cli.js.map +1 -1
  108. package/dist/schema/doctor-search-scale.d.ts +45 -0
  109. package/dist/schema/doctor.d.ts +53 -1
  110. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  111. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  112. package/dist/schema/enum-labels-kept.d.ts +26 -0
  113. package/dist/schema/generate-drizzle-schema.js +1 -1
  114. package/dist/schema/generate-files.d.ts +44 -0
  115. package/dist/schema/generate-next-step.d.ts +0 -13
  116. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  117. package/dist/schema/generate-postgres-ddl.js +10 -81
  118. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  119. package/dist/schema/generate-schema-commit.d.ts +12 -0
  120. package/dist/schema/generated-schema-diff.d.ts +7 -0
  121. package/dist/schema/introspect-db-logic.d.ts +63 -0
  122. package/dist/schema/introspect-db-queries.d.ts +21 -2
  123. package/dist/schema/introspect-db-search.d.ts +22 -0
  124. package/dist/schema/introspect-db-storage.d.ts +83 -0
  125. package/dist/schema/introspect-db-structure.d.ts +10 -0
  126. package/dist/schema/introspect-db.js +29 -21
  127. package/dist/schema/introspect-db.js.map +1 -1
  128. package/dist/schema/migrate-baseline.d.ts +33 -0
  129. package/dist/schema/migration-post-process.d.ts +34 -0
  130. package/dist/schema/plan/diff-plan.d.ts +127 -1
  131. package/dist/schema/plan/plan-schema.d.ts +16 -8
  132. package/dist/schema/plan/render-ddl.d.ts +8 -1
  133. package/dist/schema/plan/types.d.ts +45 -11
  134. package/dist/schema/push-index-prebuild.d.ts +25 -0
  135. package/dist/schema/push-legacy-renames.d.ts +20 -0
  136. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  137. package/dist/schema/undeclarable-constraints.d.ts +59 -0
  138. package/dist/search-column-BM-GV6vH.js +442 -0
  139. package/dist/search-column-BM-GV6vH.js.map +1 -0
  140. package/dist/security/policy-drift.d.ts +1 -1
  141. package/dist/security/rls-enforcement.d.ts +39 -4
  142. package/dist/services/BranchService.d.ts +29 -1
  143. package/dist/services/FetchService.d.ts +63 -12
  144. package/dist/services/PersistService.d.ts +15 -0
  145. package/dist/services/RelationService.d.ts +106 -3
  146. package/dist/services/cdc/CdcListener.d.ts +26 -10
  147. package/dist/services/cdc/identity-columns.d.ts +19 -0
  148. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  149. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  150. package/dist/services/channel-bus/index.d.ts +1 -1
  151. package/dist/services/channel-history.d.ts +32 -5
  152. package/dist/services/collection-helpers.d.ts +26 -1
  153. package/dist/services/field-viewer.d.ts +15 -1
  154. package/dist/services/pg-notify-listener.d.ts +95 -6
  155. package/dist/services/realtimeService.d.ts +467 -53
  156. package/dist/services/row-pipeline.d.ts +16 -9
  157. package/dist/services/socket-liveness.d.ts +85 -0
  158. package/dist/services/soft-delete.d.ts +12 -0
  159. package/dist/services/sql-script.d.ts +275 -0
  160. package/dist/services/vector-iterative-scan.d.ts +40 -0
  161. package/dist/services/write-depth.d.ts +22 -0
  162. package/dist/services/write-transaction-scope.d.ts +15 -3
  163. package/dist/socket-opt-ins.d.ts +64 -0
  164. package/dist/source-DB6xJv3p.js +873 -0
  165. package/dist/source-DB6xJv3p.js.map +1 -0
  166. package/dist/tsx-loader.d.ts +14 -0
  167. package/dist/utils/connection-string.d.ts +31 -0
  168. package/dist/utils/drizzle-conditions.d.ts +63 -3
  169. package/dist/utils/error-cause.d.ts +7 -0
  170. package/dist/utils/pg-error-utils.d.ts +2 -4
  171. package/dist/utils/result-rows.d.ts +10 -0
  172. package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
  173. package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
  174. package/dist/websocket-DUiiwQd2.js +1632 -0
  175. package/dist/websocket-DUiiwQd2.js.map +1 -0
  176. package/dist/websocket.d.ts +63 -18
  177. package/package.json +7 -7
  178. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  179. package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
  180. package/dist/arg-Dni7MzLB.js.map +0 -1
  181. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  182. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  183. package/dist/cli-errors-B8qHg02P.js +0 -798
  184. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  185. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  186. package/dist/cli-output-CNdMql-L.js +0 -47
  187. package/dist/cli-output-CNdMql-L.js.map +0 -1
  188. package/dist/collection-index-DdnsxiJ_.js +0 -286
  189. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  190. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  191. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  192. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  193. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  194. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  195. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  196. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  197. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  198. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  199. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  200. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  201. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  202. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  203. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  204. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  205. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  206. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  207. package/dist/source-Br7L7GOI.js +0 -410
  208. package/dist/source-Br7L7GOI.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,20 +1,41 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
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-B8qHg02P.js";
5
- import { n as outError, r as outWarn, t as out } from "./cli-output-CNdMql-L.js";
6
- import { A as resolveLocalBin, C as loadCollectionsForCli, D as readSearchDdl, E as queryGeneratedColumnDependencies, F as forLibpq, M as detectDestructiveStatements, N as splitSqlStatements, O as readTriggersDdl, P as execa, 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 decidePushSafety, 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 } from "./rls-bootstrap-sql-B8EclDyM.js";
7
- import { t as chalk } from "./source-Br7L7GOI.js";
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";
10
- import { logger } from "@rebasepro/server";
3
+ import { i as __toESM } from "./rolldown-runtime-V7rFbMRe.js";
4
+ import { A as queryColumnTypes, B as generatedSqlDir, C as getForeignIndexExcludes, D as getUndeclaredConstraintExcludes, E as getTriggerExcludes, F as readVectorDdl, G as extractPlanStatements, H as decidePushSafety, I as resolveLocalBin, J as forLibpq, K as fromAlterTable, L as withAppClient, M as queryUndeclarableConstraints, N as readSearchDdl, O as getVectorExcludes, P as readTriggersDdl, R as DESIRED_STATE_URL, S as ensureDevDatabaseExists, T as getTableExcludes, U as destructiveOptInFlag, V as prepareGeneratedSqlDir, W as detectDestructiveStatements, Y as redactConnectionPasswords, _ as describeReinstallCommand, a as formatForeignGeneratedColumnBanner, b as dropDevDatabase, c as reportCommandFailure, d as applyTriggersDdl, f as applyVectorDdl, g as describeDevAddCommand, h as describeBuildScriptRemedy, i as diagnoseDbError, j as queryGeneratedColumnDependencies, k as loadCollectionsForCli, l as ExcludeIntrospectionError, m as closeAppClients, p as atlasDevDatabaseUrl, q as isSyncedPlan, r as diagnoseAtlasFailure, t as checkDatabaseConnectivity, u as applySearchDdl, v as detectProjectPackageManager, w as getSearchExcludes, x as dropGeneratedColumns, y as diagnoseMissingBin, z as GENERATED_SQL_DIR } from "./cli-errors-oVUuD8k0.js";
5
+ import { a as outWarn, i as outError, o as atlasExcludePattern, r as out, s as excludePatternParts, t as chalk } from "./source-DB6xJv3p.js";
6
+ import { o as execa, r as dropLegacyAuthSchema, t as RLS_BOOTSTRAP_SQL } from "./rls-bootstrap-sql-Bd-hJk08.js";
7
+ import { n as require_arg, t as promptConfirm } from "./cli-prompt-egtNKkZl.js";
8
+ import { a as parseDriverLine, n as assertKnownFlags, r as collectionsPathIn } from "./cli-flags-DnPHXjRH.js";
11
9
  import { formatRelativeTime } from "@rebasepro/utils";
12
- import fs from "fs";
13
10
  import path from "path";
14
- import { fileURLToPath } from "url";
15
- //#region src/schema/generated-column-conflicts.ts
11
+ import fs from "fs";
12
+ import { fileURLToPath, pathToFileURL } from "url";
13
+ import { createRequire } from "module";
14
+ //#region src/tsx-loader.ts
16
15
  var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
17
16
  /**
17
+ * tsx's loader, as the URL `node --import` takes, or null when none resolves.
18
+ *
19
+ * For running one of this driver's scripts in one Node process. The `tsx`
20
+ * binary is a relay — pnpm's `.bin/tsx` shell shim, then tsx's CLI in one Node,
21
+ * which spawns a second Node with this same loader to run the script — so
22
+ * spawning through it costs a shell and a Node start (~50–100 ms) per child.
23
+ *
24
+ * Resolved the way Node resolves the package: from the project the driver
25
+ * runs in first (its cwd is the backend, where a scaffold declares tsx), then
26
+ * from this package. A URL rather than a path: a Windows path is not a valid
27
+ * `--import` specifier.
28
+ */
29
+ function resolveTsxLoader() {
30
+ const bases = [path.join(process.cwd(), "noop.js"), import.meta.url];
31
+ for (const base of bases) try {
32
+ return pathToFileURL(createRequire(base).resolve("tsx")).href;
33
+ } catch {}
34
+ return null;
35
+ }
36
+ //#endregion
37
+ //#region src/schema/generated-column-conflicts.ts
38
+ /**
18
39
  * Which columns an Atlas plan cannot touch, because a generated column reads
19
40
  * them.
20
41
  *
@@ -51,16 +72,6 @@ var ALTER_COLUMN_RE = new RegExp(String.raw`\bALTER\s+(?:COLUMN\s+)?(${IDENT$1})
51
72
  var DROP_COLUMN_RE$1 = new RegExp(String.raw`\bDROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(${IDENT$1})`, "gi");
52
73
  var unquote = (ident) => ident.startsWith("\"") ? ident.slice(1, -1) : ident;
53
74
  /**
54
- * Strip Atlas's plan rendering from a statement.
55
- *
56
- * What `schema apply --dry-run` prints is not a SQL file: each statement is
57
- * indented under a `-- modify "posts" table` heading and prefixed with `-> `,
58
- * with an `-- ok (12µs)` line after it. `splitSqlStatements` drops the comment
59
- * lines, and this drops the arrow — without it an anchored `^ALTER TABLE`
60
- * matches nothing at all, which is a silent no-op rather than an error.
61
- */
62
- var stripPlanMarker = (statement) => statement.replace(/^\s*->\s*/, "").trim();
63
- /**
64
75
  * Every column an Atlas plan retypes or drops, with the table it belongs to.
65
76
  *
66
77
  * One statement carries many clauses — Atlas emits `ALTER TABLE "public"."posts"
@@ -69,8 +80,8 @@ var stripPlanMarker = (statement) => statement.replace(/^\s*->\s*/, "").trim();
69
80
  */
70
81
  function parseColumnMutations(planSql) {
71
82
  const mutations = [];
72
- for (const raw of splitSqlStatements(planSql)) {
73
- const statement = stripPlanMarker(raw);
83
+ for (const raw of extractPlanStatements(planSql)) {
84
+ const statement = fromAlterTable(raw);
74
85
  const table = TABLE_RE.exec(statement);
75
86
  if (!table) continue;
76
87
  const [schema, name] = table[2] ? [unquote(table[1]), unquote(table[2])] : [void 0, unquote(table[1])];
@@ -136,7 +147,7 @@ function partitionByOwnership(conflicts, excludePatterns) {
136
147
  const managed = [];
137
148
  const foreign = [];
138
149
  for (const conflict of conflicts) {
139
- const target = `${conflict.schema}.${conflict.table}.${conflict.column}`;
150
+ const target = atlasExcludePattern(conflict.schema, conflict.table, conflict.column);
140
151
  (owned.has(target) ? managed : foreign).push(conflict);
141
152
  }
142
153
  return {
@@ -231,16 +242,17 @@ var DROP_INDEX_RE = new RegExp(`^DROP\\s+INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+
231
242
  */
232
243
  var MENTIONS_DROP = /\bDROP\b/i;
233
244
  /**
234
- * Split `schema.table.object` patterns. Anything not in three parts is
235
- * ignored rather than guessed at — the two-part form is the one Atlas itself
236
- * silently mis-reads, and repeating that mistake here would be worse than
237
- * doing nothing.
245
+ * Split `schema.table.object` patterns back into the names they were built
246
+ * from (`excludePatternParts`, the inverse of how every exclude is spelled).
247
+ * Anything not in three parts is ignored rather than guessed at — the two-part
248
+ * form is the one Atlas itself silently mis-reads, and repeating that mistake
249
+ * here would be worse than doing nothing.
238
250
  */
239
251
  function parseExcludePatterns(patterns) {
240
252
  const parsed = [];
241
253
  for (const pattern of patterns) {
242
- const parts = pattern.split(".");
243
- if (parts.length !== 3) continue;
254
+ const parts = excludePatternParts(pattern);
255
+ if (parts === void 0 || parts.length !== 3) continue;
244
256
  if (parts.some((p) => p.length === 0)) continue;
245
257
  parsed.push({
246
258
  schema: parts[0],
@@ -543,6 +555,60 @@ function tidy(sql) {
543
555
  return trimmed.length === 0 ? "" : `${trimmed}\n`;
544
556
  }
545
557
  //#endregion
558
+ //#region src/schema/migration-post-process.ts
559
+ /**
560
+ * Run the steps that finish a migration Atlas just wrote — the search, vector
561
+ * and trigger DDL, the RLS bootstrap and policies, the re-hash — and take the
562
+ * migration back out when any of them fails.
563
+ *
564
+ * Atlas's own file creates the tables and nothing else: no `ENABLE ROW LEVEL
565
+ * SECURITY`, no policy. Those arrive in the append. A failure here used to be a
566
+ * yellow warning followed by "You can now run rebase db migrate", and when it
567
+ * struck before the file was rewritten the checksum still matched — so the
568
+ * migration applied cleanly and created every new table with RLS off while
569
+ * the schema's default privileges handed `rebase_user` full DML on it. On a
570
+ * deployment that runs migrations and never provisions at boot
571
+ * (`REBASE_MIGRATE_ON_BOOT=none`), nothing closed it again.
572
+ *
573
+ * Deleting the file is safe because it is the one this run created: running
574
+ * `db generate` again, once the cause is fixed, writes the same diff.
575
+ */
576
+ async function finishNewMigration(file, steps, rehash) {
577
+ try {
578
+ await steps();
579
+ return { ok: true };
580
+ } catch (err) {
581
+ const error = err instanceof Error ? err.message : String(err);
582
+ let removed = false;
583
+ try {
584
+ if (fs.existsSync(file)) fs.unlinkSync(file);
585
+ removed = !fs.existsSync(file);
586
+ } catch {
587
+ removed = false;
588
+ }
589
+ if (!removed) return {
590
+ ok: false,
591
+ error,
592
+ removed
593
+ };
594
+ try {
595
+ await rehash();
596
+ return {
597
+ ok: false,
598
+ error,
599
+ removed
600
+ };
601
+ } catch (hashErr) {
602
+ return {
603
+ ok: false,
604
+ error,
605
+ removed,
606
+ rehashError: hashErr instanceof Error ? hashErr.message : String(hashErr)
607
+ };
608
+ }
609
+ }
610
+ }
611
+ //#endregion
546
612
  //#region src/schema/atlas-argv.ts
547
613
  /**
548
614
  * Does this Atlas invocation accept `--exclude`?
@@ -582,7 +648,7 @@ function buildAtlasArgs(invocation) {
582
648
  else if (args.includes("clean") || args.includes("inspect")) argv.push("--url", url);
583
649
  } else if (domain === "migrate") {
584
650
  if (args.includes("diff")) argv.push("--dev-url", devUrl);
585
- else if (args.includes("apply") || args.includes("status")) {
651
+ else if (args.includes("apply") || args.includes("status") || args.includes("set")) {
586
652
  argv.push("--url", url, "--revisions-schema", "rebase");
587
653
  if (args.includes("apply") && !args.includes("--baseline")) argv.push("--allow-dirty");
588
654
  }
@@ -611,6 +677,7 @@ function migrateApplyArgs(positionals, baseline) {
611
677
  var ScratchDatabase = class {
612
678
  ops;
613
679
  created;
680
+ keptAsEvidence = false;
614
681
  constructor(ops) {
615
682
  this.ops = ops;
616
683
  }
@@ -621,22 +688,205 @@ var ScratchDatabase = class {
621
688
  devDatabaseUrl
622
689
  };
623
690
  }
691
+ /** An Atlas run against it failed: keep it, whatever the command does next. */
692
+ keepAsEvidence() {
693
+ this.keptAsEvidence = true;
694
+ }
624
695
  /**
625
- * Run a command, then drop the scratch database if the command created it.
626
- * A command that throws skips the drop; one that exits the process never
627
- * returns here, which is the same thing.
696
+ * Run a command, then drop the scratch database if the command created it,
697
+ * whether the command returned or threw. A throw after
698
+ * {@link keepAsEvidence} keeps it. A command that wants to stop with exit 1
699
+ * throws {@link CommandStopped} rather than calling `process.exit`, which
700
+ * would skip this.
628
701
  */
629
702
  async around(command) {
630
- const result = await command();
631
- const created = this.created;
632
- if (created) {
633
- this.created = void 0;
634
- await this.ops.drop(created.databaseUrl, created.devDatabaseUrl);
703
+ let result;
704
+ try {
705
+ result = await command();
706
+ } catch (err) {
707
+ if (!this.keptAsEvidence) await this.release();
708
+ throw err;
635
709
  }
710
+ await this.release();
636
711
  return result;
637
712
  }
713
+ /** Drop it now if this process created it. Safe to call twice. */
714
+ async release() {
715
+ const created = this.created;
716
+ if (!created) return;
717
+ this.created = void 0;
718
+ await this.ops.drop(created.databaseUrl, created.devDatabaseUrl);
719
+ }
720
+ };
721
+ /**
722
+ * A db command stopping with exit 1 after it has said why: a destructive push
723
+ * the user declined, or one with no terminal to ask on.
724
+ *
725
+ * Thrown instead of `process.exit(1)`, so it unwinds through
726
+ * {@link ScratchDatabase.around} and the scratch database is dropped on the way
727
+ * out. `alreadyReported` tells `reportCommandFailure` the explanation is on the
728
+ * terminal already; the entry point sets the exit code.
729
+ */
730
+ var CommandStopped = class extends Error {
731
+ alreadyReported = true;
732
+ constructor(reason) {
733
+ super(reason);
734
+ this.name = "CommandStopped";
735
+ }
638
736
  };
639
737
  //#endregion
738
+ //#region src/schema/migrate-baseline.ts
739
+ /**
740
+ * What `rebase db migrate --baseline <version>` does, given the history the
741
+ * database already has.
742
+ *
743
+ * The flag promises to record `<version>` as applied and migrate from the next
744
+ * one. Atlas's own `migrate apply --baseline` keeps that promise only on a
745
+ * database with no revision history: once one row exists it ignores the flag
746
+ * without a word, applies the migration anyway, and the CLI had already printed
747
+ * "Recording … as already applied". That is the normal second time: a project
748
+ * baselines once, then `rebase dev` adds a column at boot, `db generate` writes
749
+ * the migration for it, and `db migrate` fails on `column … already exists
750
+ * (42701)` however the baseline is spelled.
751
+ *
752
+ * So the history is read first. With none, Atlas's baseline does the work.
753
+ * Behind `<version>`, `atlas migrate set` marks every migration up to it as
754
+ * applied, which is what a baseline means. At or past it, there is nothing to
755
+ * record, and the command says so instead of claiming it recorded something.
756
+ */
757
+ /**
758
+ * Atlas's version order for the versions Rebase writes: numeric timestamps,
759
+ * compared as numbers so a shorter one is not read as later; anything else as
760
+ * text.
761
+ */
762
+ function compareMigrationVersions(a, b) {
763
+ if (/^\d+$/.test(a) && /^\d+$/.test(b)) {
764
+ const x = BigInt(a);
765
+ const y = BigInt(b);
766
+ return x === y ? 0 : x < y ? -1 : 1;
767
+ }
768
+ return a.localeCompare(b);
769
+ }
770
+ /** How to keep `--baseline <version>`'s promise, given the newest recorded version. */
771
+ function planBaseline(version, latestRecorded) {
772
+ if (latestRecorded === null) return { kind: "atlas-baseline" };
773
+ if (compareMigrationVersions(version, latestRecorded) > 0) return {
774
+ kind: "set",
775
+ latest: latestRecorded
776
+ };
777
+ return {
778
+ kind: "already-recorded",
779
+ latest: latestRecorded
780
+ };
781
+ }
782
+ /** What the command says before it migrates, for each plan. */
783
+ function describeBaselinePlan(version, plan) {
784
+ switch (plan.kind) {
785
+ case "atlas-baseline": return `Recording ${version} as already applied, then migrating from the next one.`;
786
+ case "set": return `This database already records migrations up to ${plan.latest}, and Atlas applies --baseline only to a database with none.\n Marking every migration up to ${version} as applied with \`atlas migrate set\`, then migrating from the next one.`;
787
+ case "already-recorded": return `Nothing to record: this database already records migrations up to ${plan.latest}, which includes ${version}.\n Migrating from the next one.`;
788
+ }
789
+ }
790
+ /**
791
+ * The newest version Atlas has recorded on this database, or `null` when it
792
+ * has recorded none (no revisions table yet included).
793
+ *
794
+ * The table is `rebase.atlas_schema_revisions`: the CLI passes
795
+ * `--revisions-schema rebase` to every `migrate apply`.
796
+ */
797
+ async function latestRecordedMigration(databaseUrl) {
798
+ return withAppClient(databaseUrl, async (client) => {
799
+ if (!(await client.query("SELECT to_regclass('rebase.atlas_schema_revisions') IS NOT NULL AS present")).rows[0]?.present) return null;
800
+ const versions = (await client.query("SELECT version FROM rebase.atlas_schema_revisions WHERE version NOT LIKE '.%'")).rows.map((row) => row.version);
801
+ if (versions.length === 0) return null;
802
+ return versions.reduce((latest, version) => compareMigrationVersions(version, latest) > 0 ? version : latest);
803
+ });
804
+ }
805
+ //#endregion
806
+ //#region src/cli-push.ts
807
+ /**
808
+ * Apply a planned push.
809
+ *
810
+ * A dry-run that answered "Schema is synced" skips the second Atlas run: that
811
+ * run would plan the same diff again (≈0.9 s at 111 tables, 5.5 s at 411 —
812
+ * Atlas's cost grows faster than the table count) and then apply nothing. The
813
+ * steps after it still run, because Atlas cannot see what they own. A plan with
814
+ * anything in it keeps the old flow, including Atlas re-planning at apply time.
815
+ *
816
+ * @returns `"synced"` when Atlas was not run again, `"applied"` otherwise.
817
+ */
818
+ async function applyPushPlan(plan, steps) {
819
+ const synced = isSyncedPlan(plan.dryRun) && plan.rebuildable === 0;
820
+ if (synced) steps.reportSynced();
821
+ else {
822
+ if (plan.rebuildable > 0) await steps.dropRebuildable();
823
+ try {
824
+ await steps.atlasApply();
825
+ } catch (err) {
826
+ if (plan.rebuildable > 0) await steps.rebuildAfterFailure();
827
+ throw err;
828
+ }
829
+ }
830
+ await steps.afterApply();
831
+ return synced ? "synced" : "applied";
832
+ }
833
+ /**
834
+ * The tables whose stamp `db push` may write: those every one of whose planned
835
+ * statements `policies.sql` carries, verbatim.
836
+ *
837
+ * A stamp says "this stored definition is what the plan's statement made". It
838
+ * is only true of a policy the statement *did* make — and since both are
839
+ * rendered by the same function from the same collections, a table whose
840
+ * statements are all in the file is one whose policies the file just created.
841
+ * Anything else (a hand-edited file, a file generated elsewhere) is not
842
+ * stamped, and the next boot re-applies that table, as before.
843
+ */
844
+ function stampableTables(plans, policiesSql) {
845
+ return plans.filter((plan) => plan.policyStatements.length > 0 && plan.policyStatements.every((statement) => policiesSql.includes(statement)));
846
+ }
847
+ /**
848
+ * Run `policies.sql`, then stamp the policies it created the way a boot does,
849
+ * in the same transaction.
850
+ *
851
+ * Before stamps the push's policies carried none, so the first boot after
852
+ * every push re-applied them all — a DROP and a CREATE per policy, an ACCESS
853
+ * EXCLUSIVE lock per table. Stamped here, that boot finds them current.
854
+ *
855
+ * The file runs in one transaction, as it did as one implicit one: a policy
856
+ * that fails leaves the previous ones in place. The stamps run after it, under
857
+ * a savepoint: a stamp that cannot be written costs the next boot a re-apply,
858
+ * which is what it did before, so it is reported and the policies are kept.
859
+ * Nothing is left set on the connection: `SET LOCAL` ends with the transaction.
860
+ */
861
+ async function applyPoliciesFile(client, policiesSql, stamping) {
862
+ await client.query("BEGIN");
863
+ let stamped = 0;
864
+ let stampError;
865
+ try {
866
+ await client.query(policiesSql);
867
+ if (stamping.statements.length > 0) {
868
+ await client.query("SAVEPOINT rebase_policy_stamps");
869
+ try {
870
+ if (stamping.searchPath) await client.query(`SET LOCAL search_path TO ${stamping.searchPath}`);
871
+ await client.query(stamping.statements.join("\n"));
872
+ await client.query("RELEASE SAVEPOINT rebase_policy_stamps");
873
+ stamped = stamping.statements.length;
874
+ } catch (err) {
875
+ await client.query("ROLLBACK TO SAVEPOINT rebase_policy_stamps");
876
+ stampError = err instanceof Error ? err.message : String(err);
877
+ }
878
+ }
879
+ await client.query("COMMIT");
880
+ } catch (err) {
881
+ await client.query("ROLLBACK").catch(() => void 0);
882
+ throw err;
883
+ }
884
+ return {
885
+ stamped,
886
+ stampError
887
+ };
888
+ }
889
+ //#endregion
640
890
  //#region src/branch-argv.ts
641
891
  /**
642
892
  * Argument shapes for `rebase db branch`, with no dependencies.
@@ -705,8 +955,8 @@ var TAKES_A_VALUE = /* @__PURE__ */ new Set([
705
955
  * `schemaCommand`, `generatePostgresDdlCommand` and the Atlas argv assembly
706
956
  * each re-enter the loader with the same line — and then both generators wrote
707
957
  * an *empty* schema: `drizzle/schema.sql` truncated to `CREATE SCHEMA IF NOT
708
- * EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both are committed
709
- * artifacts. The push that followed planned a `DROP TABLE` for every table in
958
+ * EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both were committed
959
+ * artifacts then. The push that followed planned a `DROP TABLE` for every table in
710
960
  * the database, and the only thing between a typo and that was the destructive
711
961
  * gate.
712
962
  *
@@ -851,6 +1101,11 @@ var __cliDirname = path.dirname(fileURLToPath(import.meta.url));
851
1101
  * tsx maps it back. Running the built `.js` under plain `node` therefore fails
852
1102
  * on the user's code rather than on ours, one directory further from anything
853
1103
  * the error mentions.
1104
+ *
1105
+ * tsx's loader in one Node (`node --import`) when it resolves, rather than the
1106
+ * `tsx` binary, which is a shell shim and a relay Node in front of that same
1107
+ * loader (see `tsx-loader.ts`). The binary stays the fallback — a global tsx.
1108
+ * `args` is everything after `bin`, the script included.
854
1109
  */
855
1110
  function resolveChildScript(name) {
856
1111
  const built = path.join(__cliDirname, "schema", `${name}.js`);
@@ -860,6 +1115,16 @@ function resolveChildScript(name) {
860
1115
  ok: false,
861
1116
  reason: "script"
862
1117
  };
1118
+ const tsxLoader = resolveTsxLoader();
1119
+ if (tsxLoader) return {
1120
+ ok: true,
1121
+ bin: process.execPath,
1122
+ args: [
1123
+ "--import",
1124
+ tsxLoader,
1125
+ script
1126
+ ]
1127
+ };
863
1128
  const tsxBin = resolveLocalBin("tsx");
864
1129
  if (!tsxBin) return {
865
1130
  ok: false,
@@ -868,7 +1133,7 @@ function resolveChildScript(name) {
868
1133
  return {
869
1134
  ok: true,
870
1135
  bin: tsxBin,
871
- script
1136
+ args: [script]
872
1137
  };
873
1138
  }
874
1139
  /** What to print when {@link resolveChildScript} cannot produce a command. */
@@ -876,6 +1141,40 @@ function childScriptError(what, reason) {
876
1141
  if (reason === "tsx") return `✗ Could not find the tsx binary, which ${what} needs to read your collection files. Install your project's dependencies and try again.`;
877
1142
  return `✗ The driver's ${what} is missing. This copy of @rebasepro/server-postgres is incomplete — reinstall it, or run its build if you are working in the monorepo.`;
878
1143
  }
1144
+ /**
1145
+ * Whether this process can run the generators itself — that is, whether it
1146
+ * runs under tsx, the interpreter the project's collection files need (see
1147
+ * {@link resolveChildScript}). `rebase` always starts the driver that way when
1148
+ * tsx is installed, which a scaffold guarantees; a driver started with plain
1149
+ * `node` keeps the child process, which brings its own tsx.
1150
+ */
1151
+ function generatesInProcess() {
1152
+ return [...process.execArgv, process.env.NODE_OPTIONS ?? ""].some((flag) => /(?:^|[\s\\/=])tsx(?:$|[\s\\/@])/.test(flag));
1153
+ }
1154
+ /**
1155
+ * Run one of the generators in this process — see `schema/generate-files.ts`
1156
+ * for why a one-shot generation no longer gets a process of its own.
1157
+ *
1158
+ * The messages and the exit code are the child's: a generator that failed has
1159
+ * printed its own error, and this adds the one-line verdict; one that could not
1160
+ * even be loaded is reported the way a child that never started was.
1161
+ */
1162
+ async function generateInProcess(what, label, run) {
1163
+ let generators;
1164
+ try {
1165
+ generators = await import("./generate-files-OoRJLEdk.js").then((n) => n.t);
1166
+ } catch (err) {
1167
+ outError(chalk.red(`✗ Failed to run ${what}: ${err instanceof Error ? err.message : String(err)}`));
1168
+ process.exit(1);
1169
+ }
1170
+ try {
1171
+ await run(generators);
1172
+ } catch (err) {
1173
+ if (err instanceof generators.GenerationFailed) outError(chalk.red(`✗ ${label} generation failed — nothing was applied.`));
1174
+ else outError(chalk.red(`✗ Failed to run ${what}: ${err instanceof Error ? err.message : String(err)}`));
1175
+ process.exit(1);
1176
+ }
1177
+ }
879
1178
  async function loadEnv() {
880
1179
  try {
881
1180
  const dotenv = await import("dotenv");
@@ -904,12 +1203,19 @@ async function runPluginCommand(args) {
904
1203
  const subcommand = args[1];
905
1204
  assertKnownFlags(domain, subcommand, args);
906
1205
  assertCollectionsPathExists(collectionsPathIn(args));
907
- if (domain === "db") await scratchDatabase.around(() => dbCommand(subcommand, args));
908
- else if (domain === "schema") await schemaCommand(subcommand, args);
909
- else if (domain === "doctor") await doctorPluginCommand(args);
910
- else {
911
- outError(chalk.red(`Unknown domain command: ${domain}`));
912
- process.exit(1);
1206
+ try {
1207
+ if (domain === "db") await scratchDatabase.around(() => dbCommand(subcommand, args));
1208
+ else if (domain === "schema") await schemaCommand(subcommand, args);
1209
+ else if (domain === "doctor") await doctorPluginCommand(args);
1210
+ else if (domain === "console") {
1211
+ const { consoleCommand } = await import("./cli-console-DGee3Zl7.js");
1212
+ process.exitCode = await consoleCommand(args);
1213
+ } else {
1214
+ outError(chalk.red(`Unknown domain command: ${domain}`));
1215
+ process.exit(1);
1216
+ }
1217
+ } finally {
1218
+ await closeAppClients();
913
1219
  }
914
1220
  }
915
1221
  /** The migration files on disk, sorted, or none if the directory is absent. */
@@ -982,6 +1288,62 @@ async function stripCarvedOutFromNewestMigration(collectionsPath) {
982
1288
  return !result.empty;
983
1289
  }
984
1290
  /**
1291
+ * One line per destructive statement, as `db push` prints them — shared by
1292
+ * push, `db generate` and `db migrate --refuse-destructive`, so the three
1293
+ * describe the same statement the same way.
1294
+ */
1295
+ function destructiveLines(destructive) {
1296
+ return destructive.map((d) => chalk.red(` ${d.kind}${d.detail ? ` (${d.detail})` : ""}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1297
+ }
1298
+ /**
1299
+ * The column types `detectDestructiveStatements` reads a type change against,
1300
+ * or none when the database cannot be asked — every type change then counts
1301
+ * as destructive, which is the safe side of a report.
1302
+ */
1303
+ async function columnTypesOrNone(databaseUrl) {
1304
+ if (!databaseUrl) return [];
1305
+ try {
1306
+ return await queryColumnTypes(databaseUrl);
1307
+ } catch {
1308
+ return [];
1309
+ }
1310
+ }
1311
+ /**
1312
+ * The CHECK and EXCLUDE constraints `detectDestructiveStatements` reads a
1313
+ * `DROP CONSTRAINT` against, or none when the database cannot be asked. A
1314
+ * report then cannot tell a dropped CHECK from a dropped foreign key, and says
1315
+ * nothing about either, as before.
1316
+ */
1317
+ async function constraintsOrNone(databaseUrl) {
1318
+ if (!databaseUrl) return [];
1319
+ try {
1320
+ return await queryUndeclarableConstraints(databaseUrl);
1321
+ } catch {
1322
+ return [];
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Say what in the migration `db generate` just wrote destroys data.
1327
+ *
1328
+ * `db push` stops before a destructive change; `db generate` wrote the same
1329
+ * `DROP COLUMN` into a file without a word, and `db migrate` applied it. Only
1330
+ * the part Atlas planned is read: what is appended after it (the search
1331
+ * rebuild, policies, triggers) is Rebase's own and puts back what it drops.
1332
+ * A report, not a refusal — the exit code is unchanged.
1333
+ */
1334
+ async function reportDestructiveMigration(migrationSql, migrationFile, databaseUrl) {
1335
+ const destructive = detectDestructiveStatements(migrationSql, await columnTypesOrNone(databaseUrl), await constraintsOrNone(databaseUrl));
1336
+ if (destructive.length === 0) return;
1337
+ outWarn("");
1338
+ outWarn(chalk.yellow(` ⚠️ ${migrationFile} includes ${destructive.length} destructive change(s) that will DESTROY data when applied:`));
1339
+ outWarn("");
1340
+ for (const line of destructiveLines(destructive)) outWarn(line);
1341
+ outWarn("");
1342
+ outWarn(chalk.yellow(" Review it before `rebase db migrate`. `rebase db migrate --refuse-destructive` will not apply it."));
1343
+ outWarn(chalk.yellow(" Back up first: rebase db backup"));
1344
+ outWarn("");
1345
+ }
1346
+ /**
985
1347
  * The flags `rebase db` reads for the subcommands it parses itself. Hoisted
986
1348
  * and named `…FLAGS` so the documentation verifier, which reads specs out of
987
1349
  * this source, sees them.
@@ -989,6 +1351,7 @@ async function stripCarvedOutFromNewestMigration(collectionsPath) {
989
1351
  var DB_FLAGS = {
990
1352
  "--collections": String,
991
1353
  "--allow-destructive": Boolean,
1354
+ "--refuse-destructive": Boolean,
992
1355
  "--dry-run": Boolean,
993
1356
  "--yes": Boolean,
994
1357
  "--baseline": String,
@@ -1021,7 +1384,7 @@ async function dbCommand(subcommand, rawArgs) {
1021
1384
  return;
1022
1385
  }
1023
1386
  if (subcommand === "backup" || subcommand === "restore" || subcommand === "backups") {
1024
- const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-24v4OlSp.js");
1387
+ const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-yiKd1GsC.js");
1025
1388
  if (subcommand === "backup" && backupActionOf(rawArgs) === "list") await backupsCommand(rawArgs);
1026
1389
  else if (subcommand === "backup") await backupCommand(rawArgs);
1027
1390
  else if (subcommand === "restore") await restoreCommand(rawArgs);
@@ -1035,7 +1398,7 @@ async function dbCommand(subcommand, rawArgs) {
1035
1398
  out(chalk.bold(" 📦 Rebase DB Generate"));
1036
1399
  out(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
1037
1400
  out("");
1038
- await schemaCommand("generate", rawArgs);
1401
+ await schemaCommand("generate", rawArgs, { nextStep: false });
1039
1402
  await generatePostgresDdlCommand(rawArgs);
1040
1403
  out("");
1041
1404
  out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
@@ -1048,60 +1411,74 @@ async function dbCommand(subcommand, rawArgs) {
1048
1411
  "--dir",
1049
1412
  "file://drizzle/migrations",
1050
1413
  "--to",
1051
- "file://drizzle/schema.sql"
1414
+ DESIRED_STATE_URL
1052
1415
  ], collectionsPath);
1053
1416
  let wroteNewMigration = listMigrationFiles().length > migrationsBefore.length;
1054
1417
  if (wroteNewMigration) wroteNewMigration = await stripCarvedOutFromNewestMigration(collectionsPath);
1055
- try {
1056
- const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
1057
- if (fs.existsSync(migrationsDir) && wroteNewMigration) {
1058
- const sqlFiles = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
1059
- if (sqlFiles.length > 0) {
1060
- const newestMigrationFile = path.join(migrationsDir, sqlFiles[sqlFiles.length - 1]);
1061
- let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
1062
- migrationContent = migrationContent.replace(/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g, "CREATE SCHEMA IF NOT EXISTS $1;");
1063
- const vectorContent = readVectorDdl();
1064
- if (vectorContent) {
1065
- migrationContent = `${migrationContent}\n\n${vectorContent}`;
1066
- out(chalk.gray(" ✓ Appended vector DDL to the migration"));
1067
- }
1068
- const searchContent = readSearchDdl();
1069
- if (searchContent) {
1070
- const { searchColumnDependencies } = await import("./generate-postgres-ddl-logic-Bt2d2mRH.js").then((n) => n.o);
1071
- const migrationConflicts = findGeneratedColumnConflicts(parseColumnMutations(migrationContent), searchColumnDependencies(await loadCollectionsForCli(collectionsPath)));
1072
- const preamble = migrationDropPreamble(migrationConflicts);
1073
- if (preamble) {
1074
- migrationContent = `${preamble}\n${migrationContent}`;
1075
- for (const conflict of migrationConflicts) out(chalk.gray(` ✓ ${describeConflict(conflict)} is dropped and rebuilt inside the migration`));
1076
- }
1077
- migrationContent = `${migrationContent}\n\n${searchContent}`;
1078
- out(chalk.gray(" ✓ Appended search DDL to the migration"));
1079
- }
1080
- const triggersContent = readTriggersDdl();
1081
- if (triggersContent) {
1082
- migrationContent = `${migrationContent}\n\n${triggersContent}`;
1083
- out(chalk.gray(" ✓ Appended `updated_at` triggers to the migration"));
1084
- }
1085
- fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
1086
- const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
1087
- if (fs.existsSync(policiesFile)) {
1088
- const policiesContent = fs.readFileSync(policiesFile, "utf-8");
1089
- fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
1090
- out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
1091
- out(chalk.gray(" Re-hashing migration files..."));
1092
- await runAtlas("migrate", [
1093
- "hash",
1094
- "--dir",
1095
- "file://drizzle/migrations"
1096
- ], collectionsPath);
1097
- out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
1418
+ const newestWritten = wroteNewMigration ? listMigrationFiles().at(-1) : void 0;
1419
+ if (newestWritten) await reportDestructiveMigration(fs.readFileSync(path.resolve(process.cwd(), "drizzle", "migrations", newestWritten), "utf-8"), newestWritten, process.env.DATABASE_URL);
1420
+ const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
1421
+ const newestSql = fs.existsSync(migrationsDir) && wroteNewMigration ? fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort().at(-1) : void 0;
1422
+ if (newestSql) {
1423
+ const newestMigrationFile = path.join(migrationsDir, newestSql);
1424
+ const finished = await finishNewMigration(newestMigrationFile, async () => {
1425
+ let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
1426
+ migrationContent = migrationContent.replace(/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g, "CREATE SCHEMA IF NOT EXISTS $1;");
1427
+ const vectorContent = readVectorDdl();
1428
+ if (vectorContent) {
1429
+ migrationContent = `${migrationContent}\n\n${vectorContent}`;
1430
+ out(chalk.gray(" ✓ Appended vector DDL to the migration"));
1431
+ }
1432
+ const searchContent = readSearchDdl();
1433
+ if (searchContent) {
1434
+ const { searchColumnDependencies } = await import("./generate-postgres-ddl-logic-B4k9djAV.js").then((n) => n.o);
1435
+ const migrationConflicts = findGeneratedColumnConflicts(parseColumnMutations(migrationContent), searchColumnDependencies(await loadCollectionsForCli(collectionsPath)));
1436
+ const preamble = migrationDropPreamble(migrationConflicts);
1437
+ if (preamble) {
1438
+ migrationContent = `${preamble}\n${migrationContent}`;
1439
+ for (const conflict of migrationConflicts) out(chalk.gray(` ✓ ${describeConflict(conflict)} is dropped and rebuilt inside the migration`));
1098
1440
  }
1441
+ migrationContent = `${migrationContent}\n\n${searchContent}`;
1442
+ out(chalk.gray(" ✓ Appended search DDL to the migration"));
1443
+ }
1444
+ const triggersContent = readTriggersDdl();
1445
+ if (triggersContent) {
1446
+ migrationContent = `${migrationContent}\n\n${triggersContent}`;
1447
+ out(chalk.gray(" ✓ Appended `updated_at` triggers to the migration"));
1099
1448
  }
1449
+ fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
1450
+ const policiesFile = path.join(generatedSqlDir(), "policies.sql");
1451
+ if (fs.existsSync(policiesFile)) {
1452
+ const policiesContent = fs.readFileSync(policiesFile, "utf-8");
1453
+ fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
1454
+ out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
1455
+ out(chalk.gray(" Re-hashing migration files..."));
1456
+ await runAtlas("migrate", [
1457
+ "hash",
1458
+ "--dir",
1459
+ "file://drizzle/migrations"
1460
+ ], collectionsPath, { throwOnFailure: true });
1461
+ out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
1462
+ }
1463
+ }, async () => {
1464
+ await runAtlas("migrate", [
1465
+ "hash",
1466
+ "--dir",
1467
+ "file://drizzle/migrations"
1468
+ ], collectionsPath, { throwOnFailure: true });
1469
+ });
1470
+ if (!finished.ok) {
1471
+ outError(chalk.red(`\n ✗ Could not finish ${newestSql}: ${finished.error}`));
1472
+ outError(chalk.gray(" Atlas's half of a migration creates the tables; the part that failed adds their\n row-level security, policies, search, vector and trigger DDL. Applied alone, it\n would serve the new tables with no row filtering."));
1473
+ outError(chalk.gray(finished.removed ? ` ${newestSql} was removed. Fix the cause and run \`rebase db generate\` again.` : ` ${newestSql} could not be removed — delete it by hand before \`rebase db migrate\`.`));
1474
+ if (finished.rehashError) outError(chalk.gray(` Re-hashing failed too (${finished.rehashError}); run \`atlas migrate hash\` or generate again.`));
1475
+ process.exit(1);
1100
1476
  }
1101
- } catch (err) {
1102
- outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
1103
1477
  }
1104
- 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."));
1478
+ if (!wroteNewMigration) out(chalk.gray(` ℹ No migration written — nothing in this change is visible to Atlas.
1479
+ Search, vector, trigger and RLS DDL are applied by \`rebase db push\`, and at
1480
+ boot by the schema ensure. For a migration-only deployment, add
1481
+ ${GENERATED_SQL_DIR}/search.sql, vector.sql, triggers.sql and\n policies.sql to a migration by hand.`));
1105
1482
  out("");
1106
1483
  out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
1107
1484
  out("");
@@ -1110,32 +1487,46 @@ async function dbCommand(subcommand, rawArgs) {
1110
1487
  out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
1111
1488
  out("");
1112
1489
  if (subcommand === "push") {
1113
- out(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
1490
+ const dryRun = argsList["--dry-run"] === true;
1491
+ out(chalk.gray(dryRun ? " Step 1/3: Generating the Postgres DDL from collections (the plan's input)..." : " Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
1114
1492
  out("");
1115
- await schemaCommand("generate", rawArgs);
1116
- await generatePostgresDdlCommand(rawArgs);
1493
+ if (dryRun) out(chalk.gray(" --dry-run: src/schema.generated.ts is left as it is."));
1494
+ else await schemaCommand("generate", rawArgs, { nextStep: false });
1495
+ await generatePostgresDdlCommand(rawArgs, { retireLegacyCopies: !dryRun });
1117
1496
  out("");
1118
- const dryRun = argsList["--dry-run"] === true;
1119
1497
  out(chalk.gray(dryRun ? " Step 2/3: Planning the change against the database (nothing is applied)..." : " Step 2/3: Pushing schema to database with Atlas..."));
1120
1498
  out("");
1121
1499
  const databaseUrl = process.env.DATABASE_URL;
1122
1500
  if (databaseUrl && !dryRun) await ensureAuthSchemaAndFunctions(databaseUrl);
1501
+ if (databaseUrl) await renameLegacyForeignKeys(databaseUrl, collectionsPath, dryRun);
1502
+ const prebuilt = databaseUrl ? await prebuildForeignKeyIndexes(databaseUrl, collectionsPath, dryRun) : [];
1123
1503
  const plan = await runAtlas("schema", [
1124
1504
  "apply",
1125
1505
  "--to",
1126
- "file://drizzle/schema.sql",
1506
+ DESIRED_STATE_URL,
1127
1507
  "--dry-run"
1128
1508
  ], collectionsPath, { captureStdout: true });
1129
- const destructive = detectDestructiveStatements(plan, databaseUrl ? await queryColumnTypes(databaseUrl) : []);
1509
+ const destructive = detectDestructiveStatements(plan, databaseUrl ? await queryColumnTypes(databaseUrl) : [], databaseUrl ? await queryUndeclarableConstraints(databaseUrl) : []);
1510
+ const unreadable = destructive.some((d) => d.kind === "UNREADABLE PLAN");
1511
+ const planStatements = extractPlanStatements(plan);
1512
+ const planListing = unreadable ? plan.trim() : planStatements.map((statement) => `${statement};`).join("\n");
1130
1513
  const { managed: rebuildable, foreign: unowned } = partitionByOwnership(databaseUrl ? findGeneratedColumnConflicts(parseColumnMutations(plan), await queryGeneratedColumnDependencies(databaseUrl)) : [], collectionsPath ? await getSearchExcludes(collectionsPath) : []);
1514
+ if (databaseUrl) await reportKeptEnumLabels(databaseUrl, collectionsPath);
1131
1515
  if (dryRun) {
1132
1516
  out(chalk.gray(" Step 3/3: --dry-run — nothing was applied."));
1133
1517
  out("");
1134
- if (plan.trim()) {
1518
+ if (planStatements.length > 0 || unreadable) {
1135
1519
  out(chalk.bold(" Planned changes:"));
1136
- out(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
1520
+ out(chalk.gray(planListing.split("\n").map((l) => ` ${l}`).join("\n")));
1137
1521
  } else out(chalk.green(" ✓ No changes: the database already matches these collections."));
1138
1522
  out("");
1523
+ if (prebuilt.length > 0) {
1524
+ out(chalk.gray(` ${prebuilt.length} foreign-key index(es) on existing tables would be built first,`));
1525
+ out(chalk.gray(" CONCURRENTLY, so the apply would not build them under a write lock (the plan"));
1526
+ out(chalk.gray(" above lists them because they do not exist yet):"));
1527
+ for (const name of prebuilt) out(chalk.gray(` ${name}`));
1528
+ out("");
1529
+ }
1139
1530
  if (rebuildable.length > 0) {
1140
1531
  out(chalk.gray(` ${rebuildable.length} generated column(s) would be dropped before the apply and`));
1141
1532
  out(chalk.gray(" rebuilt from search.sql after it — Postgres cannot retype a column"));
@@ -1146,7 +1537,7 @@ async function dbCommand(subcommand, rawArgs) {
1146
1537
  if (unowned.length > 0) outWarn(formatForeignGeneratedColumnBanner(unowned));
1147
1538
  if (destructive.length > 0) {
1148
1539
  outWarn(chalk.yellow(` ⚠️ ${destructive.length} of those DESTROY data:`));
1149
- for (const d of destructive) outWarn(chalk.red(` ${d.kind}${d.detail ? ` (${d.detail})` : ""}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1540
+ for (const line of destructiveLines(destructive)) outWarn(line);
1150
1541
  outWarn("");
1151
1542
  outWarn(chalk.yellow(" Applying them needs `rebase db push --allow-destructive`. Back up first: rebase db backup"));
1152
1543
  outWarn("");
@@ -1157,72 +1548,106 @@ async function dbCommand(subcommand, rawArgs) {
1157
1548
  }
1158
1549
  if (unowned.length > 0) {
1159
1550
  outError(formatForeignGeneratedColumnBanner(unowned));
1160
- process.exit(1);
1551
+ throw new CommandStopped("A generated column Rebase does not own blocks the push.");
1161
1552
  }
1162
- const allowDestructive = argsList["--allow-destructive"] === true || argsList["--yes"] === true;
1553
+ const optIn = destructiveOptInFlag(argsList);
1163
1554
  const decision = decidePushSafety({
1164
1555
  destructiveCount: destructive.length,
1165
- allowDestructive,
1556
+ allowDestructive: optIn !== void 0,
1166
1557
  interactive: process.stdin.isTTY === true
1167
1558
  });
1168
1559
  if (destructive.length > 0) {
1169
1560
  outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
1170
1561
  outWarn("");
1171
- for (const d of destructive) outWarn(chalk.red(` ${d.kind}${d.detail ? ` (${d.detail})` : ""}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1562
+ for (const line of destructiveLines(destructive)) outWarn(line);
1172
1563
  outWarn("");
1173
1564
  outWarn(chalk.yellow(" Full planned changes:"));
1174
- outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
1565
+ outWarn(chalk.gray(planListing.split("\n").map((l) => ` ${l}`).join("\n")));
1175
1566
  outWarn("");
1176
1567
  if (decision === "refuse") {
1177
1568
  outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
1178
1569
  outError(chalk.gray(" Re-run interactively, or pass --allow-destructive to proceed. Back up first: rebase db backup"));
1179
- process.exit(1);
1570
+ throw new CommandStopped("Destructive changes need confirmation.");
1180
1571
  }
1181
1572
  if (decision === "confirm") {
1182
1573
  if (!await promptConfirm(chalk.yellow(" Type 'yes' to apply these destructive changes (this cannot be undone): "))) {
1183
1574
  out(chalk.gray(" Aborted. No changes were made."));
1184
- process.exit(1);
1575
+ throw new CommandStopped("The destructive push was declined.");
1185
1576
  }
1186
- } else outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
1187
- }
1188
- if (rebuildable.length > 0 && databaseUrl) {
1189
- for (const conflict of rebuildable) out(chalk.gray(` Dropping ${describeConflict(conflict)} — it reads a column this push retypes.`));
1190
- await dropGeneratedColumns(databaseUrl, dropGeneratedColumnStatements(rebuildable));
1577
+ } else outWarn(chalk.yellow(` Proceeding because ${optIn ?? "--allow-destructive"} was passed.`));
1191
1578
  }
1192
- try {
1193
- await runAtlas("schema", [
1194
- "apply",
1195
- "--to",
1196
- "file://drizzle/schema.sql",
1197
- "--auto-approve"
1198
- ], collectionsPath);
1199
- } catch (err) {
1200
- if (rebuildable.length > 0 && databaseUrl) {
1579
+ await applyPushPlan({
1580
+ dryRun: plan,
1581
+ rebuildable: databaseUrl ? rebuildable.length : 0
1582
+ }, {
1583
+ dropRebuildable: async () => {
1584
+ if (!databaseUrl) return;
1585
+ for (const conflict of rebuildable) out(chalk.gray(` Dropping ${describeConflict(conflict)} — it reads a column this push retypes.`));
1586
+ await dropGeneratedColumns(databaseUrl, dropGeneratedColumnStatements(rebuildable));
1587
+ },
1588
+ atlasApply: async () => {
1589
+ await runAtlas("schema", [
1590
+ "apply",
1591
+ "--to",
1592
+ DESIRED_STATE_URL,
1593
+ "--auto-approve"
1594
+ ], collectionsPath, { throwOnFailure: true });
1595
+ },
1596
+ reportSynced: () => out("Schema is synced, no changes to be made"),
1597
+ rebuildAfterFailure: async () => {
1598
+ if (!databaseUrl) return;
1201
1599
  out(chalk.gray(" The apply failed — rebuilding the generated columns it needed out of the way."));
1202
1600
  await applySearchDdl(databaseUrl);
1601
+ },
1602
+ afterApply: async () => {
1603
+ out("");
1604
+ if (!databaseUrl) {
1605
+ outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
1606
+ return;
1607
+ }
1608
+ await lockManagedTables(databaseUrl, collectionsPath);
1609
+ await ensureAuthTables(databaseUrl, collectionsPath);
1610
+ await applyVectorDdl(databaseUrl);
1611
+ await applySearchDdl(databaseUrl);
1612
+ await applyTriggersDdl(databaseUrl);
1613
+ await applyPolicies(databaseUrl, collectionsPath);
1614
+ await reconcilePolicies(databaseUrl, collectionsPath);
1615
+ await ensureRlsUserRole(databaseUrl);
1616
+ await retireLegacyAuthSchema(databaseUrl);
1203
1617
  }
1204
- throw err;
1205
- }
1206
- out("");
1207
- if (databaseUrl) {
1208
- await ensureAuthTables(databaseUrl, collectionsPath);
1209
- await applyVectorDdl(databaseUrl);
1210
- await applySearchDdl(databaseUrl);
1211
- await applyTriggersDdl(databaseUrl);
1212
- await applyPolicies(databaseUrl);
1213
- await reconcilePolicies(databaseUrl, collectionsPath);
1214
- await ensureRlsUserRole(databaseUrl);
1215
- await retireLegacyAuthSchema(databaseUrl);
1216
- } else outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
1618
+ });
1217
1619
  } else if (subcommand === "migrate") {
1218
1620
  const databaseUrl = process.env.DATABASE_URL;
1219
- if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
1220
1621
  const baseline = argsList["--baseline"];
1221
- if (baseline) {
1222
- out(chalk.gray(` Recording ${baseline} as already applied, then migrating from the next one.`));
1622
+ const baselinePlan = baseline ? planBaseline(baseline, databaseUrl ? await latestRecordedMigration(databaseUrl) : null) : null;
1623
+ const atlasBaseline = baselinePlan?.kind === "atlas-baseline" ? baseline : void 0;
1624
+ if (argsList["--refuse-destructive"] === true) {
1625
+ const plan = await runAtlas("migrate", [...migrateApplyArgs(argsList._, atlasBaseline), "--dry-run"], collectionsPath, { captureStdout: true });
1626
+ const destructive = detectDestructiveStatements(plan, await columnTypesOrNone(databaseUrl), await constraintsOrNone(databaseUrl));
1627
+ if (destructive.length > 0) {
1628
+ outError(chalk.red(` ✗ Refusing to migrate: the pending migrations include ${destructive.length} destructive change(s):`));
1629
+ outError("");
1630
+ for (const line of destructiveLines(destructive)) outError(line);
1631
+ outError("");
1632
+ outError(chalk.gray(" Nothing was applied. Run `rebase db migrate` without --refuse-destructive to apply them."));
1633
+ outError(chalk.gray(" Back up first: rebase db backup"));
1634
+ process.exit(1);
1635
+ }
1636
+ out(chalk.gray(` --refuse-destructive: ${extractPlanStatements(plan).length} pending statement(s), none destructive.`));
1223
1637
  out("");
1224
1638
  }
1225
- await runAtlas("migrate", migrateApplyArgs(argsList._, baseline), collectionsPath);
1639
+ if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
1640
+ if (baseline && baselinePlan) {
1641
+ out(chalk.gray(` ${describeBaselinePlan(baseline, baselinePlan)}`));
1642
+ out("");
1643
+ if (baselinePlan.kind === "set") await runAtlas("migrate", [
1644
+ "set",
1645
+ baseline,
1646
+ "--dir",
1647
+ "file://drizzle/migrations"
1648
+ ], collectionsPath);
1649
+ }
1650
+ await runAtlas("migrate", migrateApplyArgs(argsList._, atlasBaseline), collectionsPath);
1226
1651
  if (databaseUrl) {
1227
1652
  await ensureRlsUserRole(databaseUrl);
1228
1653
  await retireLegacyAuthSchema(databaseUrl);
@@ -1233,21 +1658,129 @@ async function dbCommand(subcommand, rawArgs) {
1233
1658
  out("");
1234
1659
  }
1235
1660
  }
1661
+ /**
1662
+ * Build, CONCURRENTLY and before Atlas plans, the derived foreign-key indexes
1663
+ * an existing table is missing — see `schema/push-index-prebuild.ts`. With
1664
+ * `dryRun`, only find them.
1665
+ *
1666
+ * Never fatal: an index not built here is one Atlas builds inside the push, as
1667
+ * it always did.
1668
+ *
1669
+ * @returns `schema.index` of each one missing when this started.
1670
+ */
1671
+ /** Renames relation columns that carry their pre-0.13 derived name, as boot does; a dry-run only names them. */
1672
+ async function renameLegacyForeignKeys(databaseUrl, collectionsPath, dryRun) {
1673
+ let renames;
1674
+ try {
1675
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1676
+ const { legacyForeignKeyRenames } = await import("./push-legacy-renames-5g9h3HOa.js");
1677
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1678
+ renames = await withAppClient(databaseUrl, (client) => legacyForeignKeyRenames({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
1679
+ } catch {
1680
+ return;
1681
+ }
1682
+ if (renames.length === 0) return;
1683
+ out(chalk.gray(dryRun ? ` ${renames.length} relation column(s) carry the name Rebase derived before 0.13; a push renames them first,` : ` Renaming ${renames.length} relation column(s) that carry the name Rebase derived before 0.13 (data, indexes and`));
1684
+ out(chalk.gray(dryRun ? " keeping their data (the plan below still shows them as a drop and an add, because nothing ran):" : " constraints are kept — the same rename `rebase dev` runs):"));
1685
+ for (const rename of renames) out(chalk.gray(` ${rename.table}: "${rename.legacy}" → "${rename.expected}"`));
1686
+ if (!dryRun) await withAppClient(databaseUrl, async (client) => {
1687
+ for (const rename of renames) await client.query(rename.sql);
1688
+ });
1689
+ out(chalk.gray(" To keep an old name instead, name the column on the relation (`localKey`, or `through` on a many-to-many)."));
1690
+ out("");
1691
+ }
1692
+ /** Names the enum labels the database keeps after the collections dropped them. See `schema/enum-labels-kept.ts`. */
1693
+ async function reportKeptEnumLabels(databaseUrl, collectionsPath) {
1694
+ try {
1695
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1696
+ const { keptEnumLabels, formatKeptEnumLabelsNotice } = await import("./enum-labels-kept-Msq5gMcb.js");
1697
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1698
+ const kept = await withAppClient(databaseUrl, (client) => keptEnumLabels({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
1699
+ if (kept.length > 0) outWarn(chalk.yellow(formatKeptEnumLabelsNotice(kept)));
1700
+ } catch {}
1701
+ }
1702
+ async function prebuildForeignKeyIndexes(databaseUrl, collectionsPath, dryRun) {
1703
+ let missing;
1704
+ try {
1705
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1706
+ const { missingForeignKeyIndexes } = await import("./push-index-prebuild-C_qhyoJa.js");
1707
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1708
+ missing = await withAppClient(databaseUrl, (client) => missingForeignKeyIndexes({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections));
1709
+ } catch {
1710
+ return [];
1711
+ }
1712
+ const names = missing.map((index) => `${index.schema}.${index.indexName}`);
1713
+ if (missing.length === 0 || dryRun) return names;
1714
+ out(chalk.gray(` Building ${missing.length} foreign-key index(es) on existing tables first — CONCURRENTLY, so writes go on:`));
1715
+ try {
1716
+ const { buildDeferredIndexes } = await import("./deferred-index-build-BqmEHGaD.js").then((n) => n.r);
1717
+ const outcome = await withAppClient(databaseUrl, (client) => buildDeferredIndexes(async () => ({
1718
+ dedicated: true,
1719
+ query: async (text) => ({ rows: (await client.query(text)).rows }),
1720
+ release: () => void 0
1721
+ }), missing, { log: (message) => out(chalk.gray(` ${message}`)) }));
1722
+ for (const failure of outcome.failed) outWarn(chalk.yellow(` ⚠️ ${failure.index} was not built ahead (${failure.error}); the push builds it inside its transaction instead.`));
1723
+ } catch (err) {
1724
+ outWarn(chalk.yellow(` ⚠️ The foreign-key indexes were not built ahead (${err instanceof Error ? err.message : String(err)}); the push builds them inside its transaction instead.`));
1725
+ }
1726
+ out("");
1727
+ return names;
1728
+ }
1236
1729
  async function ensureAuthSchemaAndFunctions(databaseUrl) {
1237
1730
  try {
1238
- const { Client } = await import("pg");
1239
- const client = new Client({ connectionString: databaseUrl });
1240
- await client.connect();
1241
- try {
1731
+ await withAppClient(databaseUrl, async (client) => {
1242
1732
  await client.query(RLS_BOOTSTRAP_SQL);
1243
- } finally {
1244
- await client.end();
1245
- }
1733
+ });
1246
1734
  } catch (err) {
1247
1735
  outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
1248
1736
  }
1249
1737
  }
1250
1738
  /**
1739
+ * Switch row-level security on for every table the collections declare, right
1740
+ * after Atlas has applied the schema.
1741
+ *
1742
+ * Atlas cannot carry `ENABLE ROW LEVEL SECURITY` in `schema.sql`: its free tier
1743
+ * neither plans it nor reads it back, so a table it creates is born with RLS
1744
+ * off — and `rebase_user`, the role every authenticated request runs as, holds
1745
+ * DML on it from that moment through the schema's default privileges. RLS used
1746
+ * to arrive with the policies, the last step of the push; a refusal from the
1747
+ * vector, search or trigger step in between left the new table readable and
1748
+ * writable by every caller, with no policy, until a later push or boot fixed
1749
+ * it — and a deployment with `REBASE_MIGRATE_ON_BOOT=none` never did. Locked
1750
+ * first, a failure later leaves a table that denies, not one that is open.
1751
+ *
1752
+ * Fatal when a table cannot be locked, and before the steps that follow: the
1753
+ * role provisioning at the end re-grants on every table in the schema, which
1754
+ * would undo the revoke that is the fallback here.
1755
+ */
1756
+ async function lockManagedTables(databaseUrl, collectionsPath) {
1757
+ let unsecured;
1758
+ let userRole;
1759
+ try {
1760
+ const { lockCollectionTables } = await import("./ensure-collection-policies-Bb51V2OV.js");
1761
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1762
+ userRole = (await import("./rls-enforcement-D7P4-ZYX.js").then((n) => n.c)).REBASE_USER_ROLE;
1763
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1764
+ ({unsecured} = await withAppClient(databaseUrl, (client) => lockCollectionTables({ query: async (text) => ({ rows: (await client.query(text)).rows }) }, collections)));
1765
+ } catch (err) {
1766
+ 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)}`));
1767
+ 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."));
1768
+ process.exit(1);
1769
+ }
1770
+ if (unsecured.length === 0) return;
1771
+ outError(chalk.red("\n ✗ The schema was applied, but row-level security could not be switched on for:"));
1772
+ for (const u of unsecured) {
1773
+ outError(chalk.red(` ${u.table}: ${u.error}`));
1774
+ 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.`));
1775
+ }
1776
+ 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:"));
1777
+ for (const u of unsecured) {
1778
+ const [schema, ...table] = u.table.split(".");
1779
+ outError(chalk.bold(` ALTER TABLE "${schema}"."${table.join(".")}" ENABLE ROW LEVEL SECURITY;`));
1780
+ }
1781
+ process.exit(1);
1782
+ }
1783
+ /**
1251
1784
  * Create the framework auth tables (e.g. `rebase.users`) that the generated RLS
1252
1785
  * policies reference, before `applyPolicies` runs. Mirrors what the server does
1253
1786
  * at boot (`PostgresBootstrapper.initializeAuth` → `ensureAuthTablesExist`).
@@ -1261,20 +1794,13 @@ async function ensureAuthSchemaAndFunctions(databaseUrl) {
1261
1794
  async function ensureAuthTables(databaseUrl, collectionsPath) {
1262
1795
  try {
1263
1796
  const { drizzle } = await import("drizzle-orm/node-postgres");
1264
- const { ensureAuthTablesExist } = await import("./ensure-tables-Dhn9KM3B.js").then((n) => n.n);
1265
- const { loadCollections } = await import("./doctor-Cb2thZ8s.js").then((n) => n.t);
1266
- const { Client } = await import("pg");
1797
+ const { ensureAuthTablesExist } = await import("./ensure-tables-DMPWhJ6R.js").then((n) => n.n);
1798
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1267
1799
  const authCollection = (await loadCollections(path.resolve(process.cwd(), collectionsPath))).find((c) => {
1268
1800
  const a = c.auth;
1269
1801
  return a === true || typeof a === "object" && a !== null && a.enabled === true;
1270
1802
  });
1271
- const client = new Client({ connectionString: databaseUrl });
1272
- await client.connect();
1273
- try {
1274
- await ensureAuthTablesExist(drizzle(client), authCollection);
1275
- } finally {
1276
- await client.end();
1277
- }
1803
+ await withAppClient(databaseUrl, (client) => ensureAuthTablesExist(drizzle(client), authCollection));
1278
1804
  } catch (err) {
1279
1805
  outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
1280
1806
  }
@@ -1289,24 +1815,19 @@ async function ensureRlsUserRole(databaseUrl) {
1289
1815
  const schemas = ["public", "rebase"];
1290
1816
  let rls;
1291
1817
  try {
1292
- rls = await import("./rls-enforcement-C6Xk0lA6.js").then((n) => n.a);
1818
+ rls = await import("./rls-enforcement-D7P4-ZYX.js").then((n) => n.c);
1293
1819
  } catch (err) {
1294
1820
  outError(chalk.red(`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`));
1295
1821
  return;
1296
1822
  }
1297
1823
  try {
1298
- const { Client } = await import("pg");
1299
- const client = new Client({ connectionString: databaseUrl });
1300
- await client.connect();
1301
- try {
1824
+ await withAppClient(databaseUrl, async (client) => {
1302
1825
  const runSql = async (text) => (await client.query(text)).rows;
1303
1826
  if ((await rls.detectConnectionPosture(runSql)).privileged) {
1304
1827
  await rls.ensureAppRole(runSql, schemas);
1305
1828
  out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
1306
1829
  }
1307
- } finally {
1308
- await client.end();
1309
- }
1830
+ });
1310
1831
  } catch (err) {
1311
1832
  outError(chalk.red(`\n ✗ The schema change was applied, but the "${rls.REBASE_USER_ROLE}" role could not be provisioned: ${err instanceof Error ? err.message : String(err)}`));
1312
1833
  outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
@@ -1323,35 +1844,23 @@ async function ensureRlsUserRole(databaseUrl) {
1323
1844
  */
1324
1845
  async function retireLegacyAuthSchema(databaseUrl) {
1325
1846
  try {
1326
- const { Client } = await import("pg");
1327
- const client = new Client({ connectionString: databaseUrl });
1328
- await client.connect();
1329
- try {
1330
- await dropLegacyAuthSchema(async (text) => (await client.query(text)).rows, {
1331
- info: (m) => out(chalk.gray(` ${m}`)),
1332
- warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
1333
- });
1334
- } finally {
1335
- await client.end();
1336
- }
1847
+ await withAppClient(databaseUrl, (client) => dropLegacyAuthSchema(async (text) => (await client.query(text)).rows, {
1848
+ info: (m) => out(chalk.gray(` ${m}`)),
1849
+ warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
1850
+ }));
1337
1851
  } catch {}
1338
1852
  }
1339
- async function applyPolicies(databaseUrl) {
1853
+ async function applyPolicies(databaseUrl, collectionsPath) {
1340
1854
  try {
1341
- const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
1855
+ const policiesPath = path.join(generatedSqlDir(), "policies.sql");
1342
1856
  if (!fs.existsSync(policiesPath)) return;
1343
1857
  out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
1344
1858
  out("");
1345
1859
  const policiesContent = fs.readFileSync(policiesPath, "utf-8");
1346
- const { Client } = await import("pg");
1347
- const client = new Client({ connectionString: databaseUrl });
1348
- await client.connect();
1349
- try {
1350
- await client.query(policiesContent);
1351
- out(chalk.green(" ✓ RLS policies applied successfully."));
1352
- } finally {
1353
- await client.end();
1354
- }
1860
+ const stamping = await policyStamping(databaseUrl, collectionsPath, policiesContent);
1861
+ const { stampError } = await withAppClient(databaseUrl, (client) => applyPoliciesFile(client, policiesContent, stamping));
1862
+ out(chalk.green(" ✓ RLS policies applied successfully."));
1863
+ if (stampError) outWarn(chalk.yellow(` ⚠️ The policies could not be stamped (${stampError}); the next boot applies them again.`));
1355
1864
  } catch (err) {
1356
1865
  const hint = diagnoseDbError(err, databaseUrl);
1357
1866
  if (hint) outError(hint);
@@ -1360,6 +1869,25 @@ async function applyPolicies(databaseUrl) {
1360
1869
  }
1361
1870
  }
1362
1871
  /**
1872
+ * The stamps a boot looks for, for the policies `policies.sql` is about to
1873
+ * create — see `applyPoliciesFile`. None when the collections cannot be read
1874
+ * here, which costs only the shortcut: the next boot re-applies, as before.
1875
+ */
1876
+ async function policyStamping(databaseUrl, collectionsPath, policiesSql) {
1877
+ try {
1878
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1879
+ const { planCollectionPolicies } = await import("./generate-postgres-ddl-logic-B4k9djAV.js").then((n) => n.o);
1880
+ const { policyStampSql } = await import("./ensure-collection-policies-Bb51V2OV.js");
1881
+ const { pinSearchPath } = await import("./connection-C938nR3E.js").then((n) => n.n);
1882
+ return {
1883
+ statements: stampableTables(planCollectionPolicies(await loadCollections(path.resolve(process.cwd(), collectionsPath))), policiesSql).map((plan) => policyStampSql(plan)).filter((statement) => statement !== void 0),
1884
+ searchPath: pinSearchPath(databaseUrl) === databaseUrl ? void 0 : "public"
1885
+ };
1886
+ } catch {
1887
+ return { statements: [] };
1888
+ }
1889
+ }
1890
+ /**
1363
1891
  * Remove the policies an earlier push superseded but never dropped.
1364
1892
  *
1365
1893
  * `policies.sql` only DROPs the names it is about to CREATE, and a rule's
@@ -1374,13 +1902,10 @@ async function applyPolicies(databaseUrl) {
1374
1902
  */
1375
1903
  async function reconcilePolicies(databaseUrl, collectionsPath) {
1376
1904
  try {
1377
- const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-DljYdrpW.js").then((n) => n.a);
1378
- const { loadCollections } = await import("./doctor-Cb2thZ8s.js").then((n) => n.t);
1905
+ const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-BKC50y2p.js").then((n) => n.a);
1906
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
1379
1907
  const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1380
- const { Client } = await import("pg");
1381
- const client = new Client({ connectionString: databaseUrl });
1382
- await client.connect();
1383
- try {
1908
+ await withAppClient(databaseUrl, async (client) => {
1384
1909
  const drift = await checkPolicyDrift(client, collections);
1385
1910
  const { dropped, kept } = await dropOrphanedPolicies(client, drift, collections);
1386
1911
  for (const p of dropped) out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
@@ -1388,7 +1913,8 @@ async function reconcilePolicies(databaseUrl, collectionsPath) {
1388
1913
  if (kept.length > 0) {
1389
1914
  outWarn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
1390
1915
  for (const p of kept) outWarn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
1391
- outWarn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
1916
+ outWarn(chalk.yellow(" Each is still in force. To keep one, describe it in its collection's `securityRules`"));
1917
+ outWarn(chalk.yellow(" (Studio's RLS view imports it); if it is stale, drop it by hand."));
1392
1918
  }
1393
1919
  const remaining = {
1394
1920
  ...drift,
@@ -1401,9 +1927,7 @@ async function reconcilePolicies(databaseUrl, collectionsPath) {
1401
1927
  orphaned: []
1402
1928
  }));
1403
1929
  }
1404
- } finally {
1405
- await client.end();
1406
- }
1930
+ });
1407
1931
  } catch (err) {
1408
1932
  outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
1409
1933
  }
@@ -1456,10 +1980,12 @@ async function branchCommand(rawArgs) {
1456
1980
  outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
1457
1981
  process.exit(1);
1458
1982
  }
1459
- const { DatabasePoolManager } = await import("./databasePoolManager-Bj5FbeAs.js").then((n) => n.n);
1460
- const { BranchService } = await import("./BranchService-DGPL6G_C.js").then((n) => n.n);
1983
+ const { DatabasePoolManager } = await import("./databasePoolManager-DZgnBOgj.js").then((n) => n.n);
1984
+ const { BranchService } = await import("./BranchService-UgKa416x.js").then((n) => n.n);
1461
1985
  const { drizzle } = await import("drizzle-orm/node-postgres");
1462
1986
  const { Pool } = await import("pg");
1987
+ await checkDatabaseConnectivity(databaseUrl);
1988
+ await closeAppClients();
1463
1989
  const pool = new Pool({
1464
1990
  connectionString: databaseUrl,
1465
1991
  max: 3
@@ -1467,8 +1993,8 @@ async function branchCommand(rawArgs) {
1467
1993
  const db = drizzle(pool);
1468
1994
  const poolManager = new DatabasePoolManager(databaseUrl);
1469
1995
  const branchService = new BranchService(db, poolManager);
1470
- await branchService.ensureBranchMetadataTable();
1471
1996
  try {
1997
+ if (branchAction === "create" || branchAction === "delete" || branchAction === "prune") await branchService.ensureBranchMetadataTable();
1472
1998
  switch (branchAction) {
1473
1999
  case "create": {
1474
2000
  const name = rawArgs[3];
@@ -1498,7 +2024,7 @@ async function branchCommand(rawArgs) {
1498
2024
  break;
1499
2025
  }
1500
2026
  case "list": {
1501
- const branches = await branchService.listBranches();
2027
+ const branches = await branchService.hasBranchRegistry() ? await branchService.listBranches() : [];
1502
2028
  out("");
1503
2029
  if (branches.length === 0) out(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
1504
2030
  else {
@@ -1535,7 +2061,7 @@ async function branchCommand(rawArgs) {
1535
2061
  out(chalk.gray(" Usage: rebase db branch info <name>"));
1536
2062
  process.exit(1);
1537
2063
  }
1538
- const info = await branchService.getBranchInfo(name);
2064
+ const info = await branchService.hasBranchRegistry() ? await branchService.getBranchInfo(name) : void 0;
1539
2065
  out("");
1540
2066
  if (!info) {
1541
2067
  outError(chalk.red(` ✗ Branch "${name}" not found.`));
@@ -1590,6 +2116,12 @@ async function branchCommand(rawArgs) {
1590
2116
  else if (plan.devDiff.length > 0) out(chalk.gray(` (${plan.devDiff.length} Atlas scratch database(s) not listed — --include-dev-diff)`));
1591
2117
  out("");
1592
2118
  if (parsed["--yes"] !== true) {
2119
+ if (!process.stdin.isTTY) {
2120
+ outError(chalk.red(" ✗ Nothing was pruned: there is no terminal to confirm on."));
2121
+ outError(chalk.gray(" Re-run with --yes to remove what is listed above."));
2122
+ outError("");
2123
+ process.exit(1);
2124
+ }
1593
2125
  if (!await promptConfirm(" Remove these? (y/N) ")) {
1594
2126
  out(chalk.gray(" Nothing was changed."));
1595
2127
  out("");
@@ -1688,6 +2220,13 @@ function timeAgo(date) {
1688
2220
  return formatRelativeTime(date, { maxMs: Number.POSITIVE_INFINITY }) ?? "unknown";
1689
2221
  }
1690
2222
  /**
2223
+ * Whether DATABASE_URL is the managed development database (PGlite), as the
2224
+ * CLI resolved it and says in `REBASE_DEV_DATABASE_KIND`.
2225
+ */
2226
+ function managedDatabase() {
2227
+ return process.env.REBASE_DEV_DATABASE_KIND === "managed";
2228
+ }
2229
+ /**
1691
2230
  * The Atlas scratch database this process created, dropped once the db
1692
2231
  * command that needed it succeeds — see `ScratchDatabase`.
1693
2232
  */
@@ -1746,8 +2285,15 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1746
2285
  process.exit(1);
1747
2286
  }
1748
2287
  await checkDatabaseConnectivity(databaseUrl);
1749
- const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
1750
- if (takesDevUrl(domain, args)) await scratchDatabase.ensure(databaseUrl, devDatabaseUrl);
2288
+ let devDatabase;
2289
+ try {
2290
+ devDatabase = atlasDevDatabaseUrl(databaseUrl, env);
2291
+ } catch (err) {
2292
+ outError(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
2293
+ process.exit(1);
2294
+ }
2295
+ const devDatabaseUrl = devDatabase.url;
2296
+ if (takesDevUrl(domain, args) && !devDatabase.named) await scratchDatabase.ensure(databaseUrl, devDatabaseUrl);
1751
2297
  const atlasUrl = forLibpq(databaseUrl);
1752
2298
  const atlasDevUrl = forLibpq(devDatabaseUrl);
1753
2299
  const excludes = [];
@@ -1764,7 +2310,7 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1764
2310
  outError(chalk.gray(" Refusing to apply — a partial exclude list could drop tables Rebase does not manage."));
1765
2311
  const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
1766
2312
  if (hint) outError(hint);
1767
- process.exit(1);
2313
+ throw new CommandStopped("Could not determine which tables to protect.");
1768
2314
  }
1769
2315
  throw err;
1770
2316
  }
@@ -1777,18 +2323,35 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1777
2323
  outError(chalk.gray(" Refusing to apply — a partial exclude list could drop an index Rebase does not manage."));
1778
2324
  const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
1779
2325
  if (hint) outError(hint);
1780
- process.exit(1);
2326
+ throw new CommandStopped("Could not determine which indexes to protect.");
2327
+ }
2328
+ throw err;
2329
+ }
2330
+ try {
2331
+ const desiredSqlFile = path.join(generatedSqlDir(), "schema.sql");
2332
+ const desiredSql = fs.existsSync(desiredSqlFile) ? fs.readFileSync(desiredSqlFile, "utf-8") : "";
2333
+ excludes.push(...await getUndeclaredConstraintExcludes(databaseUrl, desiredSql));
2334
+ } catch (err) {
2335
+ if (err instanceof ExcludeIntrospectionError) {
2336
+ outError(chalk.red("\n✗ Aborting push: could not determine which constraints to protect."));
2337
+ outError(chalk.gray(` ${err.message}`));
2338
+ outError(chalk.gray(" Refusing to apply — a partial exclude list could drop a CHECK or EXCLUDE constraint the database enforces."));
2339
+ const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
2340
+ if (hint) outError(hint);
2341
+ throw new CommandStopped("Could not determine which constraints to protect.");
1781
2342
  }
1782
2343
  throw err;
1783
2344
  }
1784
2345
  }
1785
- const subprocess = execa(atlasBin, buildAtlasArgs({
2346
+ const atlasArgs = buildAtlasArgs({
1786
2347
  domain,
1787
2348
  args,
1788
2349
  url: atlasUrl,
1789
2350
  devUrl: atlasDevUrl,
1790
2351
  excludes
1791
- }), {
2352
+ });
2353
+ if (managedDatabase()) await closeAppClients();
2354
+ const subprocess = execa(atlasBin, atlasArgs, {
1792
2355
  cwd: process.cwd(),
1793
2356
  stdout: opts.captureStdout ? "pipe" : "inherit",
1794
2357
  stderr: "pipe",
@@ -1802,26 +2365,29 @@ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1802
2365
  subprocess.stderr?.on("data", (chunk) => {
1803
2366
  const text = chunk.toString();
1804
2367
  stderrText += text;
1805
- process.stderr.write(text);
2368
+ process.stderr.write(redactConnectionPasswords(text));
1806
2369
  });
1807
2370
  try {
1808
2371
  await subprocess;
1809
2372
  } catch {
2373
+ scratchDatabase.keepAsEvidence();
1810
2374
  outError(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
1811
2375
  const hint = await diagnoseAtlasFailure({
1812
2376
  domain,
1813
2377
  args,
1814
2378
  stderr: stderrText,
1815
2379
  databaseUrl,
1816
- latestMigrationVersion: latestMigrationVersion()
2380
+ latestMigrationVersion: latestMigrationVersion(),
2381
+ managedDatabase: managedDatabase()
1817
2382
  }) ?? diagnoseDbError({ message: stderrText }, databaseUrl);
1818
2383
  if (hint) outError(hint);
2384
+ if (opts.throwOnFailure) throw Object.assign(/* @__PURE__ */ new Error(`atlas ${domain} ${args.join(" ")} failed`), { alreadyReported: true });
1819
2385
  process.exit(1);
1820
2386
  }
1821
2387
  if (opts.captureStdout) return stdoutText.trim().length > 0 ? stdoutText : stderrText;
1822
2388
  return "";
1823
2389
  }
1824
- async function generatePostgresDdlCommand(rawArgs) {
2390
+ async function generatePostgresDdlCommand(rawArgs, options = {}) {
1825
2391
  const argsList = (0, import_arg.default)({
1826
2392
  "--collections": String,
1827
2393
  "--output": String,
@@ -1831,16 +2397,31 @@ async function generatePostgresDdlCommand(rawArgs) {
1831
2397
  argv: rawArgs.slice(2),
1832
2398
  permissive: true
1833
2399
  });
1834
- const ddl = resolveChildScript("generate-postgres-ddl");
1835
- if (!ddl.ok) {
2400
+ const ddl = generatesInProcess() ? void 0 : resolveChildScript("generate-postgres-ddl");
2401
+ if (ddl && !ddl.ok) {
1836
2402
  outError(chalk.red(childScriptError("DDL generator", ddl.reason)));
1837
2403
  process.exit(1);
1838
2404
  }
1839
2405
  const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1840
- const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
2406
+ const outputPath = argsList["--output"] || path.join(".rebase/sql", "schema.sql");
2407
+ if (!argsList["--output"]) {
2408
+ const retire = options.retireLegacyCopies !== false;
2409
+ const retired = prepareGeneratedSqlDir(process.cwd(), { retire });
2410
+ if (retired.length > 0 && !retire) out(chalk.gray(` The next \`rebase db push\` or \`rebase db generate\` removes the old copies an earlier release committed:\n ${retired.join(", ")}`));
2411
+ else 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(", ")}`));
2412
+ }
2413
+ if (!ddl) {
2414
+ await generateInProcess("Postgres DDL generator", "Postgres DDL", async ({ writePostgresDdl }) => {
2415
+ await writePostgresDdl({
2416
+ collectionsPath: path.resolve(process.cwd(), collectionsPath),
2417
+ outputPath: path.resolve(process.cwd(), outputPath)
2418
+ });
2419
+ });
2420
+ return;
2421
+ }
1841
2422
  const cmdParts = [
1842
2423
  ddl.bin,
1843
- ddl.script,
2424
+ ...ddl.args,
1844
2425
  `--collections=${collectionsPath}`,
1845
2426
  `--output=${outputPath}`
1846
2427
  ];
@@ -1889,9 +2470,9 @@ async function schemaStaleCommand(rawArgs) {
1889
2470
  const schemaFile = path.resolve(process.cwd(), outputPath);
1890
2471
  const fix = Boolean(argsList["--fix"]);
1891
2472
  const generatedExists = fs.existsSync(schemaFile);
1892
- const { loadCollections } = await import("./doctor-Cb2thZ8s.js").then((n) => n.t);
1893
- const { findLegacyForeignKeyNames, compareGeneratedDeclarations, staleVerdict } = await import("./generated-schema-staleness-Di9c4ZxN.js").then((n) => n.r);
1894
- const { generateSchema } = await import("./generate-drizzle-schema-logic-BfzK7UQd.js").then((n) => n.n);
2473
+ const { loadCollections } = await import("./doctor-CUAAOFZm.js").then((n) => n.t);
2474
+ const { findLegacyForeignKeyNames, compareGeneratedDeclarations, staleVerdict } = await import("./generated-schema-staleness-BWN_PsMi.js").then((n) => n.r);
2475
+ const { generateSchema } = await import("./generate-drizzle-schema-logic-BaT0H7Gq.js").then((n) => n.n);
1895
2476
  const { relationalCollections } = await import("@rebasepro/common");
1896
2477
  let stale = [];
1897
2478
  let differences = [];
@@ -1904,6 +2485,7 @@ async function schemaStaleCommand(rawArgs) {
1904
2485
  if (postgresCollections.length > 0) differences = compareGeneratedDeclarations(generateSchema(postgresCollections), onDisk);
1905
2486
  } catch (err) {
1906
2487
  unreadable = err instanceof Error ? err.message : String(err);
2488
+ const { logger } = await import("@rebasepro/server");
1907
2489
  logger.debug(`schema stale: skipped (${unreadable})`);
1908
2490
  }
1909
2491
  const verdict = staleVerdict({
@@ -1928,7 +2510,7 @@ async function schemaStaleCommand(rawArgs) {
1928
2510
  `--output=${outputPath}`
1929
2511
  ]);
1930
2512
  }
1931
- async function schemaCommand(subcommand, rawArgs) {
2513
+ async function schemaCommand(subcommand, rawArgs, options = {}) {
1932
2514
  if (subcommand && (rawArgs.includes("--help") || rawArgs.includes("-h"))) {
1933
2515
  out("");
1934
2516
  out(chalk.bold(` rebase schema ${subcommand}`));
@@ -1953,24 +2535,38 @@ async function schemaCommand(subcommand, rawArgs) {
1953
2535
  argv: rawArgs.slice(2),
1954
2536
  permissive: true
1955
2537
  });
2538
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
2539
+ const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
2540
+ const watch = argsList["--watch"] || false;
2541
+ if (!watch && generatesInProcess()) {
2542
+ out("");
2543
+ out(chalk.bold(" 🔧 Rebase Schema Generator"));
2544
+ out("");
2545
+ await generateInProcess("schema generator", "Drizzle schema", async ({ writeDrizzleSchema }) => {
2546
+ await writeDrizzleSchema({
2547
+ collectionsPath: path.resolve(process.cwd(), collectionsPath),
2548
+ outputPath: path.resolve(process.cwd(), outputPath),
2549
+ nextStep: options.nextStep
2550
+ });
2551
+ });
2552
+ return;
2553
+ }
1956
2554
  const generator = resolveChildScript("generate-drizzle-schema");
1957
2555
  if (!generator.ok) {
1958
2556
  outError(chalk.red(childScriptError("schema generator", generator.reason)));
1959
2557
  process.exit(1);
1960
2558
  }
1961
- const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1962
- const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
1963
- const watch = argsList["--watch"] || false;
1964
2559
  out("");
1965
2560
  out(chalk.bold(" 🔧 Rebase Schema Generator"));
1966
2561
  out("");
1967
2562
  const cmdParts = [
1968
2563
  generator.bin,
1969
- generator.script,
2564
+ ...generator.args,
1970
2565
  `--collections=${collectionsPath}`,
1971
2566
  `--output=${outputPath}`
1972
2567
  ];
1973
2568
  if (watch) cmdParts.push("--watch");
2569
+ if (options.nextStep === false) cmdParts.push("--no-next-step");
1974
2570
  try {
1975
2571
  await execa(cmdParts[0], cmdParts.slice(1), {
1976
2572
  cwd: process.cwd(),
@@ -2007,7 +2603,7 @@ async function schemaCommand(subcommand, rawArgs) {
2007
2603
  out("");
2008
2604
  const cmdParts = [
2009
2605
  introspect.bin,
2010
- introspect.script,
2606
+ ...introspect.args,
2011
2607
  `--output=${outputPath}`,
2012
2608
  ...argsList["--force"] ? ["--force"] : [],
2013
2609
  ...argsList["--schema"] ? [`--schema=${argsList["--schema"]}`] : []
@@ -2019,7 +2615,7 @@ async function schemaCommand(subcommand, rawArgs) {
2019
2615
  env: { ...process.env }
2020
2616
  });
2021
2617
  } catch (err) {
2022
- outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
2618
+ if (typeof (typeof err === "object" && err !== null ? Reflect.get(err, "exitCode") : void 0) !== "number") outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
2023
2619
  process.exit(1);
2024
2620
  }
2025
2621
  } else {
@@ -2050,7 +2646,7 @@ async function doctorPluginCommand(rawArgs) {
2050
2646
  const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
2051
2647
  const cmdParts = [
2052
2648
  doctor.bin,
2053
- doctor.script,
2649
+ ...doctor.args,
2054
2650
  `--collections=${collectionsPath}`,
2055
2651
  `--schema=${schemaPath}`,
2056
2652
  `--sdk=${sdkPath}`,