@rebasepro/server-postgres 0.24.1-canary.gcb918fc → 0.25.0-canary.ge06257e

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 (137) hide show
  1. package/dist/{BranchService-Bsn2wfaj.js → BranchService-DNg4txyf.js} +14 -4
  2. package/dist/BranchService-DNg4txyf.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +13 -0
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/backup/backup-cli.d.ts +88 -2
  6. package/dist/backup/backup-service.d.ts +11 -1
  7. package/dist/backup/pg-tools.d.ts +18 -0
  8. package/dist/{backup-cli-DnpJi6I7.js → backup-cli-HCEkPcQj.js} +215 -79
  9. package/dist/backup-cli-HCEkPcQj.js.map +1 -0
  10. package/dist/{backup-service-BTBB7RXn.js → backup-service-D-UpT7DH.js} +121 -11
  11. package/dist/backup-service-D-UpT7DH.js.map +1 -0
  12. package/dist/{boot-ddl-DVpsFN6r.js → boot-ddl-Dnvpmm_T.js} +3 -3
  13. package/dist/{boot-ddl-DVpsFN6r.js.map → boot-ddl-Dnvpmm_T.js.map} +1 -1
  14. package/dist/branch-argv.d.ts +18 -6
  15. package/dist/cli-console-C_ISGwJo.js +255 -0
  16. package/dist/cli-console-C_ISGwJo.js.map +1 -0
  17. package/dist/cli-console.d.ts +10 -0
  18. package/dist/{cli-errors-CaI4ntyD.js → cli-errors-mx4sSjB8.js} +477 -103
  19. package/dist/cli-errors-mx4sSjB8.js.map +1 -0
  20. package/dist/cli-errors.d.ts +40 -4
  21. package/dist/{cli-flags-JNI-pwSf.js → cli-flags-DUozBFyK.js} +70 -6
  22. package/dist/cli-flags-DUozBFyK.js.map +1 -0
  23. package/dist/cli-flags.d.ts +21 -0
  24. package/dist/cli-helpers.d.ts +53 -24
  25. package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
  26. package/dist/cli-prompt-egtNKkZl.js.map +1 -0
  27. package/dist/cli-prompt.d.ts +19 -0
  28. package/dist/cli-scratch-database.d.ts +47 -8
  29. package/dist/cli.js +548 -94
  30. package/dist/cli.js.map +1 -1
  31. package/dist/{column-plan-helpers-drZIsSNf.js → column-plan-helpers-D_V4oaZh.js} +2 -2
  32. package/dist/{column-plan-helpers-drZIsSNf.js.map → column-plan-helpers-D_V4oaZh.js.map} +1 -1
  33. package/dist/{connection-C9mT8bgf.js → connection-C938nR3E.js} +2 -2
  34. package/dist/{connection-C9mT8bgf.js.map → connection-C938nR3E.js.map} +1 -1
  35. package/dist/{databasePoolManager-BIuewidb.js → databasePoolManager-Yqdw14wY.js} +4 -4
  36. package/dist/{databasePoolManager-BIuewidb.js.map → databasePoolManager-Yqdw14wY.js.map} +1 -1
  37. package/dist/{deferred-index-build-DP1_SNFo.js → deferred-index-build-Ca6OqKyR.js} +8 -8
  38. package/dist/{deferred-index-build-DP1_SNFo.js.map → deferred-index-build-Ca6OqKyR.js.map} +1 -1
  39. package/dist/{doctor-C4WiftHm.js → doctor-CJyuhV1n.js} +8 -8
  40. package/dist/{doctor-C4WiftHm.js.map → doctor-CJyuhV1n.js.map} +1 -1
  41. package/dist/{ensure-collection-policies-CXpbeNag.js → ensure-collection-policies-C1ux3V_z.js} +79 -18
  42. package/dist/ensure-collection-policies-C1ux3V_z.js.map +1 -0
  43. package/dist/{ensure-collection-tables-BhsU6qUE.js → ensure-collection-tables-Do-Bvb64.js} +10 -10
  44. package/dist/{ensure-collection-tables-BhsU6qUE.js.map → ensure-collection-tables-Do-Bvb64.js.map} +1 -1
  45. package/dist/{ensure-tables-CVm_yvzb.js → ensure-tables-Dl_Eh9ow.js} +5 -5
  46. package/dist/{ensure-tables-CVm_yvzb.js.map → ensure-tables-Dl_Eh9ow.js.map} +1 -1
  47. package/dist/{enum-labels-kept-D8fI1MoX.js → enum-labels-kept-D9eTIWCz.js} +3 -3
  48. package/dist/{enum-labels-kept-D8fI1MoX.js.map → enum-labels-kept-D9eTIWCz.js.map} +1 -1
  49. package/dist/{generate-drizzle-schema-Bz52jH2h.js → generate-drizzle-schema-Bf8canaR.js} +3 -3
  50. package/dist/{generate-drizzle-schema-Bz52jH2h.js.map → generate-drizzle-schema-Bf8canaR.js.map} +1 -1
  51. package/dist/{generate-drizzle-schema-logic-B74WN37S.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +4 -4
  52. package/dist/{generate-drizzle-schema-logic-B74WN37S.js.map → generate-drizzle-schema-logic-BaT0H7Gq.js.map} +1 -1
  53. package/dist/{generate-files-CdEckX6c.js → generate-files-D50Pxw1G.js} +6 -47
  54. package/dist/generate-files-D50Pxw1G.js.map +1 -0
  55. package/dist/generate-next-step-DCd7yiJ2.js +66 -0
  56. package/dist/generate-next-step-DCd7yiJ2.js.map +1 -0
  57. package/dist/{generate-postgres-ddl-logic-B64O2oZF.js → generate-postgres-ddl-logic-B4k9djAV.js} +4 -4
  58. package/dist/{generate-postgres-ddl-logic-B64O2oZF.js.map → generate-postgres-ddl-logic-B4k9djAV.js.map} +1 -1
  59. package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-FlxM2i4i.js} +102 -12
  60. package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-FlxM2i4i.js.map} +1 -1
  61. package/dist/index.es.js +11 -11
  62. package/dist/{introspect-db-logic-C-Di8dA9.js → introspect-db-logic-j2vYhmdP.js} +145 -9
  63. package/dist/introspect-db-logic-j2vYhmdP.js.map +1 -0
  64. package/dist/{introspect-db-queries-D6kAqReI.js → introspect-db-queries-DevUrzll.js} +41 -6
  65. package/dist/introspect-db-queries-DevUrzll.js.map +1 -0
  66. package/dist/{open-rebase-CbOur0TU.js → open-rebase-D8rytTP0.js} +95 -45
  67. package/dist/open-rebase-D8rytTP0.js.map +1 -0
  68. package/dist/open-rebase.d.ts +12 -4
  69. package/dist/{pg-error-utils-vJezlXdI.js → pg-error-utils-CdsGYMTm.js} +10 -3
  70. package/dist/pg-error-utils-CdsGYMTm.js.map +1 -0
  71. package/dist/{plan-schema-B1YF_tYq.js → plan-schema-CsNgK0qi.js} +4 -4
  72. package/dist/{plan-schema-B1YF_tYq.js.map → plan-schema-CsNgK0qi.js.map} +1 -1
  73. package/dist/{policy-drift-Dm8i-s1J.js → policy-drift-CY5P1ZRu.js} +28 -14
  74. package/dist/{policy-drift-Dm8i-s1J.js.map → policy-drift-CY5P1ZRu.js.map} +1 -1
  75. package/dist/{push-index-prebuild-Db4AQF4i.js → push-index-prebuild-BDFcfRPo.js} +4 -4
  76. package/dist/{push-index-prebuild-Db4AQF4i.js.map → push-index-prebuild-BDFcfRPo.js.map} +1 -1
  77. package/dist/{push-legacy-renames-BK6ilQq2.js → push-legacy-renames-D4bvMWAb.js} +4 -4
  78. package/dist/{push-legacy-renames-BK6ilQq2.js.map → push-legacy-renames-D4bvMWAb.js.map} +1 -1
  79. package/dist/push-policy-plan-DTXE6aVa.js +111 -0
  80. package/dist/push-policy-plan-DTXE6aVa.js.map +1 -0
  81. package/dist/{render-ddl-CtC3y9Ol.js → render-ddl-CrfEe__B.js} +10 -9
  82. package/dist/render-ddl-CrfEe__B.js.map +1 -0
  83. package/dist/{rls-bootstrap-sql-RQUljnJ1.js → rls-bootstrap-sql-Bd-hJk08.js} +3 -58
  84. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
  85. package/dist/{rls-enforcement-DvzqlaL1.js → rls-enforcement-D7P4-ZYX.js} +2 -2
  86. package/dist/{rls-enforcement-DvzqlaL1.js.map → rls-enforcement-D7P4-ZYX.js.map} +1 -1
  87. package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
  88. package/dist/schema/atlas-apply-report.d.ts +23 -0
  89. package/dist/schema/atlas-argv.d.ts +22 -1
  90. package/dist/schema/atlas-exclude.d.ts +42 -0
  91. package/dist/schema/atlas-revisions.d.ts +4 -0
  92. package/dist/schema/carved-out-migration.d.ts +5 -4
  93. package/dist/schema/destructive-sql.d.ts +23 -1
  94. package/dist/schema/doctor-cli.js +5 -5
  95. package/dist/schema/ensure-collection-policies.d.ts +50 -0
  96. package/dist/schema/generate-drizzle-schema.js +1 -1
  97. package/dist/schema/generate-next-step.d.ts +11 -0
  98. package/dist/schema/generate-postgres-ddl.js +2 -2
  99. package/dist/schema/generated-schema-diff.d.ts +7 -0
  100. package/dist/schema/generated-schema-staleness.d.ts +7 -1
  101. package/dist/schema/introspect-db-logic.d.ts +32 -1
  102. package/dist/schema/introspect-db-queries.d.ts +18 -5
  103. package/dist/schema/introspect-db.js +53 -52
  104. package/dist/schema/introspect-db.js.map +1 -1
  105. package/dist/schema/migrate-baseline.d.ts +51 -0
  106. package/dist/schema/plan/render-ddl.d.ts +2 -1
  107. package/dist/schema/push-policy-plan.d.ts +67 -0
  108. package/dist/schema/undeclarable-constraints.d.ts +59 -0
  109. package/dist/security/policy-drift.d.ts +5 -0
  110. package/dist/services/BranchService.d.ts +7 -0
  111. package/dist/{source-DBDbBTL4.js → source-DB6xJv3p.js} +65 -3
  112. package/dist/source-DB6xJv3p.js.map +1 -0
  113. package/dist/utils/connection-string.d.ts +47 -0
  114. package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
  115. package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
  116. package/dist/{websocket-BLGc5yhs.js → websocket-DMeXOoyv.js} +41 -13
  117. package/dist/websocket-DMeXOoyv.js.map +1 -0
  118. package/package.json +7 -7
  119. package/dist/BranchService-Bsn2wfaj.js.map +0 -1
  120. package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
  121. package/dist/arg-Dni7MzLB.js.map +0 -1
  122. package/dist/backup-cli-DnpJi6I7.js.map +0 -1
  123. package/dist/backup-service-BTBB7RXn.js.map +0 -1
  124. package/dist/cli-console-Ca6ZlAVU.js +0 -154
  125. package/dist/cli-console-Ca6ZlAVU.js.map +0 -1
  126. package/dist/cli-errors-CaI4ntyD.js.map +0 -1
  127. package/dist/cli-flags-JNI-pwSf.js.map +0 -1
  128. package/dist/ensure-collection-policies-CXpbeNag.js.map +0 -1
  129. package/dist/generate-files-CdEckX6c.js.map +0 -1
  130. package/dist/introspect-db-logic-C-Di8dA9.js.map +0 -1
  131. package/dist/introspect-db-queries-D6kAqReI.js.map +0 -1
  132. package/dist/open-rebase-CbOur0TU.js.map +0 -1
  133. package/dist/pg-error-utils-vJezlXdI.js.map +0 -1
  134. package/dist/render-ddl-CtC3y9Ol.js.map +0 -1
  135. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +0 -1
  136. package/dist/source-DBDbBTL4.js.map +0 -1
  137. package/dist/websocket-BLGc5yhs.js.map +0 -1
@@ -1,7 +1,6 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { a as __toESM, t as require___vite_browser_external } from "./__vite-browser-external-BnuHet1e.js";
4
- import { a as outWarn, f as isRebaseIndexName, i as outError, r as out, t as chalk } from "./source-DBDbBTL4.js";
3
+ import { a as outWarn, i as outError, m as isRebaseIndexName, o as atlasExcludePattern, r as out, s as excludePatternParts, t as chalk } from "./source-DB6xJv3p.js";
5
4
  import path from "path";
6
5
  import fs from "fs";
7
6
  import { fileURLToPath, pathToFileURL } from "url";
@@ -25,33 +24,165 @@ function extractCauseMessage(error) {
25
24
  return null;
26
25
  }
27
26
  //#endregion
28
- //#region src/schema/destructive-sql.ts
29
- var import___vite_browser_external = /* @__PURE__ */ __toESM(require___vite_browser_external(), 1);
27
+ //#region src/utils/connection-string.ts
30
28
  /**
31
- * Pure helpers that classify an Atlas declarative-apply plan as destructive
32
- * or not, and decide what `rebase db push` should do about it.
29
+ * Connection-string rewrites for the tools Rebase shells out to.
33
30
  *
34
- * `db push` runs `atlas schema apply` to make the live database match the
35
- * generated `schema.sql`. Removing a collection field compiles to
36
- * `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
37
- * We first run the apply with `--dry-run` to obtain the planned SQL, scan it
38
- * here, and refuse to auto-approve anything destructive without an explicit
39
- * opt-in.
31
+ * Pure and dependency-free: `pg-tools` builds argument vectors without a live
32
+ * server, and these have to be usable from there.
33
+ */
34
+ /**
35
+ * The `sslmode` values libpq accepts. `no-verify` is conspicuously not one.
36
+ */
37
+ var LIBPQ_SSLMODES = /* @__PURE__ */ new Set([
38
+ "disable",
39
+ "allow",
40
+ "prefer",
41
+ "require",
42
+ "verify-ca",
43
+ "verify-full"
44
+ ]);
45
+ /**
46
+ * Make a connection string safe to hand to a libpq program.
47
+ *
48
+ * `sslmode=no-verify` is a node-postgres convention — encrypt, but do not check
49
+ * the certificate — and libpq does not have it. It does not degrade or warn:
40
50
  *
41
- * A type change is a destructive plan that names no DROP. `ALTER COLUMN
42
- * "price" TYPE integer` rounds every 19.99 to 20, and `TYPE date` throws away
43
- * every timestamp's time of day, in a statement that reads like a routine
44
- * alter. So a type change is gated too, unless it is a widening every value
45
- * survives unchanged — which needs the column's current type, read from the
46
- * catalogue by the caller.
51
+ * psql: error: invalid sslmode value: "no-verify"
47
52
  *
48
- * A primary key change names no DROP of data either: `DROP CONSTRAINT
49
- * "<table>_pkey", ADD PRIMARY KEY ("id", "locale")` keeps every value and
50
- * gives every row a new address. It is gated like a drop.
53
+ * which means a `DATABASE_URL` that works perfectly for the app makes `psql`,
54
+ * `pg_dump`, `pg_restore` and Atlas all refuse to start, with an error that
55
+ * points at the value rather than at the convention it comes from. It has cost
56
+ * this project time more than once.
51
57
  *
52
- * Everything in this file is side-effect free so it can be unit-tested
53
- * without Atlas or a database.
58
+ * `require` is the honest translation: libpq's `require` encrypts and does not
59
+ * verify the certificate either, which is exactly what `no-verify` asks for.
60
+ * Nothing is relaxed by the rewrite — `verify-ca` and `verify-full` are left
61
+ * alone, so a connection string that asked for verification still gets it.
62
+ *
63
+ * Only `sslmode` is touched, and only when it is a value libpq would reject.
64
+ * Anything unparseable is returned as given: a connection that works
65
+ * unverified beats one this corrupted.
54
66
  */
67
+ function forLibpq(connectionString) {
68
+ let url;
69
+ try {
70
+ url = new URL(connectionString);
71
+ } catch {
72
+ return connectionString;
73
+ }
74
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return connectionString;
75
+ const sslmode = url.searchParams.get("sslmode");
76
+ if (sslmode === null || LIBPQ_SSLMODES.has(sslmode)) return connectionString;
77
+ url.searchParams.set("sslmode", "require");
78
+ url.search = Array.from(url.searchParams.entries()).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
79
+ return url.toString();
80
+ }
81
+ /** Hosts that are this machine: `localhost`, `127.0.0.0/8`, `::1` and `0.0.0.0`. */
82
+ var isLoopbackHost = (hostname) => hostname === "localhost" || hostname === "[::1]" || hostname === "0.0.0.0" || /^127(\.\d{1,3}){3}$/.test(hostname);
83
+ /**
84
+ * A connection string for Atlas: {@link forLibpq}, and `sslmode=disable` for a
85
+ * database on this machine whose URL says nothing about SSL.
86
+ *
87
+ * Atlas connects through lib/pq, whose default `sslmode` is `require`.
88
+ * node-postgres — the app, and every step of these commands — asks for no SSL
89
+ * when the URL names none. So a URL that served the app from a local Postgres
90
+ * without SSL (the Docker image, a Homebrew install) failed every schema
91
+ * command with `pq: SSL is not enabled on the server`. Given here, Atlas
92
+ * connects the way the app already does.
93
+ *
94
+ * Nothing that asked for something is touched: a URL naming `sslmode`, `ssl`,
95
+ * `host` or `hostaddr`, any other host, and anything that is not a postgres
96
+ * URL are returned as {@link forLibpq} returns them.
97
+ */
98
+ function forAtlas(connectionString) {
99
+ const libpq = forLibpq(connectionString);
100
+ let url;
101
+ try {
102
+ url = new URL(libpq);
103
+ } catch {
104
+ return libpq;
105
+ }
106
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return libpq;
107
+ if (!isLoopbackHost(url.hostname)) return libpq;
108
+ if ([
109
+ "sslmode",
110
+ "ssl",
111
+ "host",
112
+ "hostaddr"
113
+ ].some((key) => url.searchParams.has(key))) return libpq;
114
+ url.searchParams.set("sslmode", "disable");
115
+ url.search = Array.from(url.searchParams.entries()).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
116
+ return url.toString();
117
+ }
118
+ /**
119
+ * A connection string with its password taken out, and the password.
120
+ *
121
+ * For a libpq program: the URL goes on its command line, and the password goes
122
+ * in `PGPASSWORD`. On the command line it was readable by anyone who can list
123
+ * processes, and it came back verbatim in every failure, because execa's error
124
+ * message repeats the command: `Command failed with exit code 1: pg_restore
125
+ * '--dbname=postgresql://app:s3cret@…'`, in a terminal, a CI log and the
126
+ * scheduled backup's run history.
127
+ *
128
+ * libpq takes the password from the URL's user info or a `password` parameter,
129
+ * so both are removed. The value is percent-decoded, because `PGPASSWORD` is
130
+ * read literally. Anything that is not a URL is returned as given, with no
131
+ * password: a key/value DSN keeps its own.
132
+ */
133
+ function splitPassword(connectionString) {
134
+ let url;
135
+ try {
136
+ url = new URL(connectionString);
137
+ } catch {
138
+ return {
139
+ connectionString,
140
+ password: null
141
+ };
142
+ }
143
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return {
144
+ connectionString,
145
+ password: null
146
+ };
147
+ const fromUserInfo = url.password ? decodePercent(url.password) : null;
148
+ const fromParameter = url.searchParams.get("password");
149
+ if (fromUserInfo === null && fromParameter === null) return {
150
+ connectionString,
151
+ password: null
152
+ };
153
+ url.password = "";
154
+ if (fromParameter !== null) {
155
+ url.searchParams.delete("password");
156
+ url.search = Array.from(url.searchParams.entries()).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
157
+ }
158
+ return {
159
+ connectionString: url.toString(),
160
+ password: fromUserInfo ?? fromParameter
161
+ };
162
+ }
163
+ function decodePercent(value) {
164
+ try {
165
+ return decodeURIComponent(value);
166
+ } catch {
167
+ return value;
168
+ }
169
+ }
170
+ /**
171
+ * Every password a line of text carries in a connection string, replaced with
172
+ * `***`.
173
+ *
174
+ * For whatever is about to be printed or logged and may have a connection
175
+ * string in it: an error a client tool or a driver raised, a message that
176
+ * quotes a command line. Covers the URL user-info form (`scheme://user:pass@`,
177
+ * any scheme, a password with a raw `@` in it included) and `password=` in a
178
+ * query string or a key/value DSN. The user, host and database stay, because
179
+ * they are what a reader needs to tell which database it was.
180
+ */
181
+ function redactConnectionPasswords(text) {
182
+ return text.replace(/(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#@'"]*):[^\s/?#'"]*@/gi, "$1:***@").replace(/(\bpassword\s*=\s*)('[^']*'|[^\s&'"]+)/gi, "$1***");
183
+ }
184
+ //#endregion
185
+ //#region src/schema/destructive-sql.ts
55
186
  /**
56
187
  * SQL fragments that destroy data or data-bearing objects. Matched
57
188
  * case-insensitively against each statement of the plan. `IF EXISTS` /
@@ -222,8 +353,12 @@ var COLUMN_TYPES_SQL = `
222
353
  * `columnTypes` is what the database holds now. A type change to a column it
223
354
  * does not list is flagged: not knowing what a column is means not knowing
224
355
  * whether the change keeps its values.
356
+ *
357
+ * `constraints` are the CHECK and EXCLUDE constraints the database holds now.
358
+ * A `DROP CONSTRAINT` of one of them is flagged; of any other constraint (a
359
+ * foreign key, a `UNIQUE`) it is not, as before.
225
360
  */
226
- function detectDestructiveStatements(planSql, columnTypes = []) {
361
+ function detectDestructiveStatements(planSql, columnTypes = [], constraints = []) {
227
362
  const found = [];
228
363
  const { statements, announced } = readPlan(planSql);
229
364
  if (announced !== void 0 && announced !== statements.length) found.push({
@@ -238,9 +373,14 @@ function detectDestructiveStatements(planSql, columnTypes = []) {
238
373
  change,
239
374
  from: currentTypeOf(change, columnTypes)
240
375
  })).filter(({ change, from }) => from === void 0 || !isLosslessTypeChange(from, change.to)).map(({ change, from }) => `"${change.column}" ${from ?? "(current type unknown)"} → ${change.to}`).join("; ");
241
- const kind = dropped?.label ?? (rekeyed ? "PRIMARY KEY CHANGE" : lossy ? "ALTER COLUMN TYPE" : void 0);
376
+ const unenforced = describeConstraintDrops(statement, constraints);
377
+ const kind = dropped?.label ?? (rekeyed ? "PRIMARY KEY CHANGE" : lossy ? "ALTER COLUMN TYPE" : unenforced ? "DROP CONSTRAINT" : void 0);
242
378
  if (!kind) continue;
243
- const detail = [rekeyed, lossy].filter(Boolean).join("; ");
379
+ const detail = [
380
+ rekeyed,
381
+ lossy,
382
+ unenforced
383
+ ].filter(Boolean).join("; ");
244
384
  found.push(detail ? {
245
385
  statement,
246
386
  kind,
@@ -354,6 +494,30 @@ function describePrimaryKeyChange(statement) {
354
494
  if (dropped) return `${table} loses its primary key ${dropped}: ${ROW_ADDRESS_LOST}`;
355
495
  }
356
496
  /**
497
+ * The CHECK and EXCLUDE constraints an `ALTER TABLE` drops, in a sentence — or
498
+ * `undefined` when it drops none.
499
+ *
500
+ * Matched by name on the statement's table, in its schema when the plan names
501
+ * one (Atlas always does). A constraint the catalogue does not list as a CHECK
502
+ * or an EXCLUDE — a foreign key, a `UNIQUE`, the primary key — is not this.
503
+ */
504
+ function describeConstraintDrops(statement, constraints) {
505
+ if (constraints.length === 0) return void 0;
506
+ const alter = ALTER_TABLE_RE.exec(fromAlterTable(statement));
507
+ if (!alter) return void 0;
508
+ const [schema, table] = alter[2] ? [unquote(alter[1]), unquote(alter[2])] : [void 0, unquote(alter[1])];
509
+ const lost = [];
510
+ for (const clause of splitClauses(alter[3])) {
511
+ const drop = DROP_CONSTRAINT_RE.exec(clause);
512
+ if (!drop) continue;
513
+ const name = unquote(drop[1]);
514
+ const constraint = constraints.find((c) => c.table === table && c.name === name && (schema === void 0 || c.schema === schema));
515
+ if (constraint) lost.push(`${constraint.kind === "check" ? "CHECK" : "EXCLUDE"} "${name}"`);
516
+ }
517
+ if (lost.length === 0) return void 0;
518
+ return `${schema ? `${schema}.${table}` : table} loses ${lost.join(", ")}: no row is lost, and the database accepts every row ${lost.length === 1 ? "it refuses" : "they refuse"} today`;
519
+ }
520
+ /**
357
521
  * The column's current type. A table named without its schema matches in any
358
522
  * schema, but only when every match agrees — two same-named tables with
359
523
  * different types leave the answer unknown.
@@ -468,6 +632,17 @@ function isLosslessTypeChange(from, to) {
468
632
  return false;
469
633
  }
470
634
  /**
635
+ * The flag that opted a push into destructive changes, as it was given —
636
+ * `--allow-destructive`, or its alias `--yes` / `-y` — or `undefined` for
637
+ * none. The line that says why a destructive push went ahead names the one
638
+ * the user typed: "because --allow-destructive was passed" to someone who
639
+ * typed `--yes` reads like a flag they never set.
640
+ */
641
+ function destructiveOptInFlag(args) {
642
+ if (args["--allow-destructive"] === true) return "--allow-destructive";
643
+ if (args["--yes"] === true) return "--yes";
644
+ }
645
+ /**
471
646
  * Decide how `db push` should proceed given the plan's destructiveness and
472
647
  * the invocation context.
473
648
  *
@@ -483,6 +658,78 @@ function decidePushSafety(opts) {
483
658
  return opts.interactive ? "confirm" : "refuse";
484
659
  }
485
660
  //#endregion
661
+ //#region src/schema/undeclarable-constraints.ts
662
+ /**
663
+ * The constraints a collection cannot declare, and how `db push` keeps Atlas
664
+ * from dropping them.
665
+ *
666
+ * `db push` is declarative: Atlas makes each managed table match `schema.sql`,
667
+ * and anything on the table that `schema.sql` does not mention is planned as a
668
+ * drop. The collection model declares a table's primary key, its columns'
669
+ * `NOT NULL`, `UNIQUE` and foreign keys, and nothing else. So a hand-written
670
+ * `CHECK (total >= 0)` — or one an adopted database arrived with, which
671
+ * `schema introspect` reads as `validation.min: 0` — was dropped by the first
672
+ * push, in the same `ALTER TABLE` as whatever else the push changed, with no
673
+ * prompt. From then on the database accepted every row the constraint used to
674
+ * refuse, while the form still enforced the rule and so hid that it had gone.
675
+ *
676
+ * EXCLUDE constraints are the same case: Atlas inspects them and plans their
677
+ * drop the same way. Triggers and functions are not: Atlas's free tier does
678
+ * not inspect either, so it never plans a drop for one. Measured on the pinned
679
+ * atlas 1.2.3 against PG 18; `introspect-check-constraints-e2e` pins all four.
680
+ *
681
+ * Ownership, as with indexes (`getForeignIndexExcludes` in `cli-helpers.ts`)
682
+ * and policies: a constraint the desired state declares is
683
+ * Atlas's to manage, and every other CHECK or EXCLUDE is somebody else's and is
684
+ * kept out of the plan with `--exclude`. Today the desired state declares
685
+ * neither kind, so every one is kept. A rule nobody wants any more is dropped
686
+ * by hand (`ALTER TABLE … DROP CONSTRAINT …`), which is the only statement that
687
+ * can say so.
688
+ *
689
+ * Side-effect free, so it is unit-tested without Atlas or a database.
690
+ */
691
+ /**
692
+ * Every CHECK and EXCLUDE constraint on an ordinary or partitioned table
693
+ * outside the system schemas.
694
+ *
695
+ * `contype` `c` and `x` only. A primary key (`p`), `UNIQUE` (`u`), foreign key
696
+ * (`f`) and, from PG 18, a catalogued `NOT NULL` (`n`) are what a collection
697
+ * declares, so Atlas keeps managing them.
698
+ */
699
+ var UNDECLARABLE_CONSTRAINTS_SQL = `
700
+ SELECT n.nspname AS "schema",
701
+ c.relname AS "table",
702
+ con.conname AS "name",
703
+ CASE con.contype WHEN 'c' THEN 'check' ELSE 'exclude' END AS "kind"
704
+ FROM pg_constraint con
705
+ JOIN pg_class c ON c.oid = con.conrelid
706
+ JOIN pg_namespace n ON n.oid = c.relnamespace
707
+ WHERE con.contype IN ('c', 'x')
708
+ AND c.relkind IN ('r', 'p')
709
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
710
+ AND n.nspname NOT LIKE 'pg\\_%'
711
+ `;
712
+ var DECLARED_CONSTRAINT_RE = /\bCONSTRAINT\s+"((?:[^"]|"")+)"\s+(?:CHECK|EXCLUDE)\b/gi;
713
+ /**
714
+ * The CHECK and EXCLUDE constraint names a desired state (`schema.sql`)
715
+ * declares — Atlas's to manage, and so never excluded.
716
+ */
717
+ function declaredConstraintNames(desiredSql) {
718
+ const names = /* @__PURE__ */ new Set();
719
+ for (const match of desiredSql.matchAll(DECLARED_CONSTRAINT_RE)) names.add(match[1].replace(/""/g, "\""));
720
+ return names;
721
+ }
722
+ /**
723
+ * The `--exclude` patterns that keep Atlas from dropping the CHECK and EXCLUDE
724
+ * constraints the desired state does not declare. Three-part
725
+ * `schema.table.constraint`, which is what Atlas matches a constraint by: a
726
+ * two-part pattern reads as a table, matches nothing, and reports no error.
727
+ */
728
+ function undeclaredConstraintExcludes(existing, desiredSql) {
729
+ const declared = declaredConstraintNames(desiredSql);
730
+ return existing.filter((constraint) => !declared.has(constraint.name)).map((constraint) => atlasExcludePattern(constraint.schema, constraint.table, constraint.name));
731
+ }
732
+ //#endregion
486
733
  //#region src/module-dir.ts
487
734
  var moduleDir = path.dirname(fileURLToPath(import.meta.url));
488
735
  //#endregion
@@ -655,15 +902,53 @@ function describeReinstallCommand(manager = detectProjectPackageManager()) {
655
902
  default: return "pnpm install --force";
656
903
  }
657
904
  }
658
- /** `add <pkg>` as a dev dependency, in the reader's own package manager. */
659
- function describeDevAddCommand(packageName, manager = detectProjectPackageManager()) {
905
+ /**
906
+ * The workspace member a command runs in, relative to the workspace's root —
907
+ * `backend` in a scaffolded project — or null at that root and outside any
908
+ * workspace.
909
+ *
910
+ * The member is the nearest directory at or above `startDir` holding a
911
+ * `package.json`; the root is the nearest one above it holding a
912
+ * `pnpm-workspace.yaml`, or a `package.json` that declares `workspaces`.
913
+ */
914
+ function workspaceMemberDir(startDir = process.cwd()) {
915
+ let member = path.resolve(startDir);
916
+ while (!fs.existsSync(path.join(member, "package.json"))) {
917
+ const parent = path.dirname(member);
918
+ if (parent === member) return null;
919
+ member = parent;
920
+ }
921
+ for (let dir = path.dirname(member);; dir = path.dirname(dir)) {
922
+ if (isWorkspaceRoot(dir)) return path.relative(dir, member).split(path.sep).join("/");
923
+ if (path.dirname(dir) === dir) return null;
924
+ }
925
+ }
926
+ function isWorkspaceRoot(dir) {
927
+ if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return true;
928
+ try {
929
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
930
+ return typeof manifest === "object" && manifest !== null && "workspaces" in manifest;
931
+ } catch {
932
+ return false;
933
+ }
934
+ }
935
+ /**
936
+ * `add <pkg>` as a dev dependency, in the reader's own package manager, typed
937
+ * from the project root: into `workspaceDir` when the command runs in a
938
+ * workspace member, where a bare `pnpm add -D` at the root refuses with
939
+ * `ERR_PNPM_ADDING_TO_ROOT`.
940
+ */
941
+ function describeDevAddCommand(packageName, manager = detectProjectPackageManager(), workspaceDir = workspaceMemberDir()) {
942
+ const dir = workspaceDir === null ? null : shellWord(workspaceDir);
660
943
  switch (manager) {
661
- case "npm": return `npm install -D ${packageName}`;
662
- case "yarn": return `yarn add -D ${packageName}`;
663
- case "bun": return `bun add -d ${packageName}`;
664
- default: return `pnpm add -D ${packageName}`;
944
+ case "npm": return dir ? `npm install -D ${packageName} -w ${dir}` : `npm install -D ${packageName}`;
945
+ case "yarn": return dir ? `yarn --cwd ${dir} add -D ${packageName}` : `yarn add -D ${packageName}`;
946
+ case "bun": return dir ? `bun add -d ${packageName} --cwd ${dir}` : `bun add -d ${packageName}`;
947
+ default: return dir ? `pnpm --dir ${dir} add -D ${packageName}` : `pnpm add -D ${packageName}`;
665
948
  }
666
949
  }
950
+ /** A path as one shell word: as it is when it needs no quoting, single-quoted otherwise. */
951
+ var shellWord = (value) => /^[\w./-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
667
952
  /**
668
953
  * Where a package's own manifest lives, or null if it is not installed here.
669
954
  *
@@ -1007,7 +1292,7 @@ async function applySearchDdl(databaseUrl, sqlDir = generatedSqlDir()) {
1007
1292
  * `search` block, which is every project that has not opted in.
1008
1293
  */
1009
1294
  async function getSearchExcludes(collectionsPath) {
1010
- const { searchExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1295
+ const { searchExcludePatterns } = await import("./generate-postgres-ddl-logic-B4k9djAV.js").then((n) => n.o);
1011
1296
  return searchExcludePatterns(await loadCollectionsForCli(collectionsPath));
1012
1297
  }
1013
1298
  /**
@@ -1094,7 +1379,7 @@ function readVectorDdl(sqlDir = generatedSqlDir()) {
1094
1379
  async function applyVectorDdl(databaseUrl, sqlDir = generatedSqlDir()) {
1095
1380
  const sql = readVectorDdl(sqlDir);
1096
1381
  if (!sql) return;
1097
- const { vectorExtensionHint } = await import("./vector-index-CMJKb3C9.js").then((n) => n.f);
1382
+ const { vectorExtensionHint } = await import("./vector-index-BFhyEaiz.js").then((n) => n.f);
1098
1383
  try {
1099
1384
  await withAppClient(databaseUrl, (client) => client.query(sql));
1100
1385
  } catch (err) {
@@ -1112,7 +1397,7 @@ async function applyVectorDdl(databaseUrl, sqlDir = generatedSqlDir()) {
1112
1397
  * `vector` property, which is every project that has not opted in.
1113
1398
  */
1114
1399
  async function getVectorExcludes(collectionsPath) {
1115
- const { vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1400
+ const { vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-B4k9djAV.js").then((n) => n.o);
1116
1401
  return vectorExcludePatterns(await loadCollectionsForCli(collectionsPath));
1117
1402
  }
1118
1403
  /**
@@ -1149,17 +1434,12 @@ async function applyTriggersDdl(databaseUrl, sqlDir = generatedSqlDir()) {
1149
1434
  }
1150
1435
  /** @see getSearchExcludes */
1151
1436
  async function getTriggerExcludes(collectionsPath) {
1152
- const { triggerExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1437
+ const { triggerExcludePatterns } = await import("./generate-postgres-ddl-logic-B4k9djAV.js").then((n) => n.o);
1153
1438
  return triggerExcludePatterns(await loadCollectionsForCli(collectionsPath));
1154
1439
  }
1155
1440
  /**
1156
- * Query the live database for every user table/view outside the system
1157
- * catalogs. Separated from {@link getTableExcludes} so its failure mode can
1158
- * be handled explicitly (fail closed) and so tests can inject a stub.
1159
- */
1160
- /**
1161
- * Every index Postgres holds on a table Rebase manages, as
1162
- * `schema.table.index`.
1441
+ * Every index Postgres holds on a table, by its parts — never joined with `.`,
1442
+ * which a name may itself contain.
1163
1443
  *
1164
1444
  * Constraint-backed indexes are left out: a `PRIMARY KEY` or a `UNIQUE` is a
1165
1445
  * *constraint* to Atlas, diffed from the constraint declaration, and naming its
@@ -1168,7 +1448,7 @@ async function getTriggerExcludes(collectionsPath) {
1168
1448
  async function queryExistingIndexes(databaseUrl) {
1169
1449
  return withAppClient(databaseUrl, async (client) => {
1170
1450
  return (await client.query(`
1171
- SELECT n.nspname || '.' || t.relname || '.' || i.relname AS full_name
1451
+ SELECT n.nspname AS "schema", t.relname AS "table", i.relname AS "index"
1172
1452
  FROM pg_index x
1173
1453
  JOIN pg_class i ON i.oid = x.indexrelid
1174
1454
  JOIN pg_class t ON t.oid = x.indrelid
@@ -1178,7 +1458,7 @@ async function queryExistingIndexes(databaseUrl) {
1178
1458
  AND NOT EXISTS (
1179
1459
  SELECT 1 FROM pg_constraint c WHERE c.conindid = i.oid
1180
1460
  );
1181
- `)).rows.map((row) => row.full_name);
1461
+ `)).rows;
1182
1462
  });
1183
1463
  }
1184
1464
  /**
@@ -1207,7 +1487,9 @@ async function queryExistingIndexes(databaseUrl) {
1207
1487
  *
1208
1488
  * Three-part `schema.table.index`, never two: a two-part pattern reads as a
1209
1489
  * *table* named `<index>` in a *schema* named `<table>`, matches nothing, and
1210
- * reports no error — the trap already recorded for the search excludes.
1490
+ * reports no error — the trap already recorded for the search excludes. And
1491
+ * spelled by `atlasExcludePattern`: a raw `orders_idx[v1]` is a glob that
1492
+ * matches `orders_idxv`, so that index was dropped with no prompt.
1211
1493
  */
1212
1494
  async function getForeignIndexExcludes(databaseUrl, collectionsPath, deps = {}) {
1213
1495
  const queryIndexes = deps.queryExistingIndexes ?? queryExistingIndexes;
@@ -1218,11 +1500,10 @@ async function getForeignIndexExcludes(databaseUrl, collectionsPath, deps = {})
1218
1500
  } catch (err) {
1219
1501
  throw new ExcludeIntrospectionError(`Failed to introspect the database for unmanaged indexes: ${err instanceof Error ? err.message : String(err)}`, err);
1220
1502
  }
1221
- return existing.filter((full) => {
1222
- const indexName = full.slice(full.lastIndexOf(".") + 1);
1223
- if (managed.has(indexName)) return false;
1224
- return !isRebaseIndexName(indexName);
1225
- });
1503
+ return existing.filter(({ index }) => {
1504
+ if (managed.has(index)) return false;
1505
+ return !isRebaseIndexName(index);
1506
+ }).map(({ schema, table, index }) => atlasExcludePattern(schema, table, index));
1226
1507
  }
1227
1508
  /**
1228
1509
  * The index names the current collections would create — search, vector and
@@ -1230,21 +1511,48 @@ async function getForeignIndexExcludes(databaseUrl, collectionsPath, deps = {})
1230
1511
  */
1231
1512
  async function managedIndexNames(collectionsPath) {
1232
1513
  const collections = await loadCollectionsForCli(collectionsPath);
1233
- const { searchExcludePatterns, vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1234
- const { resolveColumnName } = await import("./column-plan-helpers-drZIsSNf.js").then((n) => n.t);
1235
- const { buildCollectionIndexPlan } = await import("./source-DBDbBTL4.js").then((n) => n.l);
1514
+ const { searchExcludePatterns, vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-B4k9djAV.js").then((n) => n.o);
1515
+ const { resolveColumnName } = await import("./column-plan-helpers-D_V4oaZh.js").then((n) => n.t);
1516
+ const { buildCollectionIndexPlan } = await import("./source-DB6xJv3p.js").then((n) => n.d);
1236
1517
  const names = new Set(buildCollectionIndexPlan(collections, resolveColumnName).map((spec) => spec.indexName));
1237
- for (const pattern of [...searchExcludePatterns(collections), ...vectorExcludePatterns(collections)]) names.add(pattern.slice(pattern.lastIndexOf(".") + 1));
1518
+ for (const pattern of [...searchExcludePatterns(collections), ...vectorExcludePatterns(collections)]) {
1519
+ const object = excludePatternParts(pattern)?.[2];
1520
+ if (object !== void 0) names.add(object);
1521
+ }
1238
1522
  return names;
1239
1523
  }
1524
+ /** Every CHECK and EXCLUDE constraint the database holds. @see UNDECLARABLE_CONSTRAINTS_SQL */
1525
+ async function queryUndeclarableConstraints(databaseUrl) {
1526
+ return withAppClient(databaseUrl, async (client) => (await client.query(UNDECLARABLE_CONSTRAINTS_SQL)).rows);
1527
+ }
1528
+ /**
1529
+ * Patterns keeping Atlas away from the CHECK and EXCLUDE constraints the
1530
+ * collections do not declare — which, since a collection cannot declare
1531
+ * either, is all of them. See `schema/undeclarable-constraints.ts`.
1532
+ *
1533
+ * Fails CLOSED, like {@link getTableExcludes}: without the catalogue nothing
1534
+ * says which constraints are there, and a partial list lets the push drop a
1535
+ * rule the database enforces. `desiredSql` is the `schema.sql` Atlas is about
1536
+ * to diff against; a constraint it declares is Atlas's to manage.
1537
+ */
1538
+ async function getUndeclaredConstraintExcludes(databaseUrl, desiredSql, deps = {}) {
1539
+ const queryConstraints = deps.queryConstraints ?? queryUndeclarableConstraints;
1540
+ let existing;
1541
+ try {
1542
+ existing = await queryConstraints(databaseUrl);
1543
+ } catch (err) {
1544
+ throw new ExcludeIntrospectionError(`Failed to introspect the database for CHECK and EXCLUDE constraints: ${err instanceof Error ? err.message : String(err)}`, err);
1545
+ }
1546
+ return undeclaredConstraintExcludes(existing, desiredSql);
1547
+ }
1240
1548
  async function queryExistingTables(databaseUrl) {
1241
1549
  return withAppClient(databaseUrl, async (client) => {
1242
1550
  return (await client.query(`
1243
- SELECT table_schema || '.' || table_name AS full_name
1551
+ SELECT table_schema AS "schema", table_name AS "table"
1244
1552
  FROM information_schema.tables
1245
1553
  WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
1246
1554
  AND table_type IN ('BASE TABLE', 'VIEW');
1247
- `)).rows.map((row) => row.full_name);
1555
+ `)).rows;
1248
1556
  });
1249
1557
  }
1250
1558
  /**
@@ -1291,36 +1599,9 @@ async function getTableExcludes(databaseUrl, collectionsPath, deps = {}) {
1291
1599
  } catch (err) {
1292
1600
  throw new ExcludeIntrospectionError(`Failed to introspect the database for unmapped tables: ${err instanceof Error ? err.message : String(err)}`, err);
1293
1601
  }
1294
- for (const table of existingTables) if (!includes.includes(table)) excludes.push(table);
1602
+ for (const { schema, table } of existingTables) if (!includes.includes(`${schema}.${table}`)) excludes.push(atlasExcludePattern(schema, table));
1295
1603
  return excludes;
1296
1604
  }
1297
- /**
1298
- * Ask a yes/no question on an interactive terminal.
1299
- *
1300
- * The `isTTY` guard is the contract, not an optimisation: non-interactive
1301
- * shells (CI, pipes, agents) can never answer, and `readline` on a
1302
- * non-TTY stdin resolves with whatever the pipe happens to contain — or never
1303
- * resolves at all. Returning false there is what makes an unattended
1304
- * `db push` abort instead of auto-confirming a destructive change. Callers
1305
- * should already have gone through {@link decidePushSafety}, which decides
1306
- * whether interactive confirmation is even possible; this is the backstop.
1307
- *
1308
- * Lives here rather than in cli.ts so it is reachable from a test — cli.ts uses
1309
- * `import.meta` and cannot be imported by the jest runner.
1310
- */
1311
- async function promptConfirm(question) {
1312
- if (!process.stdin.isTTY) return false;
1313
- const rl = import___vite_browser_external.default.createInterface({
1314
- input: process.stdin,
1315
- output: process.stdout
1316
- });
1317
- try {
1318
- const answer = await new Promise((resolve) => rl.question(question, resolve));
1319
- return /^y(es)?$/i.test(answer.trim());
1320
- } finally {
1321
- rl.close();
1322
- }
1323
- }
1324
1605
  //#endregion
1325
1606
  //#region src/cli-errors.ts
1326
1607
  /**
@@ -1430,12 +1711,49 @@ function driverReasonLine(err) {
1430
1711
  return reason ? ` The driver said: ${reason}\n\n` : "";
1431
1712
  }
1432
1713
  /**
1714
+ * What named the database a db command connects to, as the CLI resolved it
1715
+ * and says in `REBASE_DATABASE_SOURCE`: `flag` (`--database-url`),
1716
+ * `environment` (the shell's DATABASE_URL), `env-file` (`.env`), `branch` (a
1717
+ * `db branch switch`), `docker` / `last-dev` (the compose `db` service) or
1718
+ * `managed` (the managed development database). `undefined` when nothing
1719
+ * says — a boot, or the driver run by hand.
1720
+ */
1721
+ function databaseSource() {
1722
+ const source = process.env.REBASE_DATABASE_SOURCE?.trim();
1723
+ return source ? source : void 0;
1724
+ }
1725
+ /** Where the connection string is, in the words a reader looks for it by. */
1726
+ function connectionStringPlace(source) {
1727
+ switch (source) {
1728
+ case "flag": return "the URL passed with --database-url";
1729
+ case "environment": return "DATABASE_URL in this shell's environment";
1730
+ default: return "DATABASE_URL";
1731
+ }
1732
+ }
1733
+ /** The fixes for a database that does not answer, for what named it. */
1734
+ function connectionRefusedFixes(source) {
1735
+ switch (source) {
1736
+ case "flag": return ["Check the URL passed with --database-url, and that its server is running"];
1737
+ case "environment": return ["Check DATABASE_URL in this shell's environment, and that its server is running"];
1738
+ case "env-file": return ["Verify DATABASE_URL in your .env file, and that its server is running"];
1739
+ case "branch": return ["The branch's database is on the server DATABASE_URL in .env names: start that server", "Or leave the branch: rebase db branch switch --off"];
1740
+ case "docker":
1741
+ case "last-dev": return ["docker compose up -d db (the project's Postgres in Docker)"];
1742
+ case "managed": return ["Restart the managed development database: rebase db stop, then run the command again"];
1743
+ default: return [
1744
+ "docker compose up -d db (the service a Rebase scaffold ships)",
1745
+ "brew services start postgresql@18",
1746
+ "Verify DATABASE_URL in your .env file"
1747
+ ];
1748
+ }
1749
+ }
1750
+ /**
1433
1751
  * Format a diagnostic banner for ECONNREFUSED errors.
1434
1752
  */
1435
1753
  function formatConnectionRefusedBanner(databaseUrl, err) {
1436
1754
  return `
1437
1755
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1438
- ❌ Cannot connect to PostgreSQL at ${parseHostInfo(databaseUrl)}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + driverReasonLine(err) + " The database server is not running or is not accepting\n connections. Common fixes:\n\n • docker compose up -d db (the service a Rebase scaffold ships)\n • brew services start postgresql@18\n • Verify DATABASE_URL in your .env file\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
1756
+ ❌ Cannot connect to PostgreSQL at ${parseHostInfo(databaseUrl)}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + driverReasonLine(err) + " The database server is not running or is not accepting\n connections. Common fixes:\n\n" + connectionRefusedFixes(databaseSource()).map((fix) => ` • ${fix}\n`).join("") + "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
1439
1757
  }
1440
1758
  /**
1441
1759
  * Format a diagnostic banner for authentication failures.
@@ -1450,9 +1768,7 @@ function formatAuthFailureBanner(databaseUrl, err) {
1450
1768
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1451
1769
  ❌ Authentication failed for user "${username}" at ${hostInfo}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + driverReasonLine(err) + ` PostgreSQL rejected the credentials. Common fixes:
1452
1770
 
1453
- • Check the username and password in DATABASE_URL
1454
- • Verify the user exists: psql -c "\\du"
1455
- • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1771
+ • Check the username and password in ${connectionStringPlace(databaseSource())}\n • Verify the user exists: psql -c "\\du"\n • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1456
1772
  }
1457
1773
  /**
1458
1774
  * Format a diagnostic banner for "SSL is not enabled on the server".
@@ -1545,50 +1861,107 @@ function diagnoseDbError(err, databaseUrl) {
1545
1861
  * it resolved to and the cwd it resolved against, and then throws so the entry
1546
1862
  * point owns the exit code. Repeating its one-line summary underneath would
1547
1863
  * undo the "printed once" this whole path exists for.
1864
+ *
1865
+ * Whatever is printed has its connection-string passwords replaced (see
1866
+ * `redactConnectionPasswords`): a driver or tool error can quote the URL it was
1867
+ * given, and this line lands in terminals and CI logs.
1548
1868
  */
1549
1869
  function reportCommandFailure(error) {
1550
- const message = error instanceof Error ? error.message : String(error ?? "");
1870
+ const message = redactConnectionPasswords(error instanceof Error ? error.message : String(error ?? ""));
1551
1871
  if (error?.alreadyReported) return;
1552
1872
  if (!message || /Command failed|exited with code/i.test(message)) return;
1553
1873
  outError("");
1554
1874
  outError(chalk.red(` ✗ ${message}`));
1555
- const cause = extractCauseMessage(error);
1875
+ const rawCause = extractCauseMessage(error);
1876
+ const cause = rawCause ? redactConnectionPasswords(rawCause) : rawCause;
1556
1877
  if (cause && !message.includes(cause)) outError(chalk.gray(` ${cause}`));
1557
1878
  outError("");
1558
1879
  }
1880
+ /** The table Atlas keeps its migration history in, in the `rebase` schema. */
1881
+ var ATLAS_REVISIONS_TABLE = "atlas_schema_revisions";
1559
1882
  /**
1560
1883
  * `migrate apply` refusing because the database already has the schema.
1561
1884
  *
1562
- * `42710` (duplicate_object) and `42P07` (duplicate_table) are what a migration
1563
- * hits when boot-ensure — or a `db push` — has already provisioned the objects
1564
- * it was going to create. Since boot-ensure provisions *every* production
1885
+ * `42710` (duplicate_object), `42P07` (duplicate_table), `42701`
1886
+ * (duplicate_column) and `42P06` (duplicate_schema) are what a migration hits
1887
+ * when boot-ensure — or a `db push` — has already provisioned the objects it
1888
+ * was going to create. Since boot-ensure provisions *every* production
1565
1889
  * database, this is the normal case rather than the exotic one, and the raw
1566
1890
  * failure (`pq: type "posts_status" already exists (42710)`, then `sql/migrate:
1567
1891
  * write revision: … current transaction is aborted`) names no way forward.
1892
+ *
1893
+ * `42701` is the everyday one in development: `rebase dev` adds a new
1894
+ * column at boot, `db generate` writes the migration that adds it, and
1895
+ * `db migrate` then finds it there. A column is named `table.column`.
1568
1896
  */
1569
1897
  function parseAlreadyProvisioned(text) {
1570
- const withCode = /(?:type|relation|constraint|schema) "([^"]+)" already exists \((42710|42P07)\)/.exec(text);
1898
+ const column = /column "([^"]+)" of relation "([^"]+)" already exists/.exec(text);
1899
+ if (column && /42701/.test(text)) return {
1900
+ object: `${column[2]}.${column[1]}`,
1901
+ code: "42701"
1902
+ };
1903
+ const withCode = /(?:type|relation|constraint|schema) "([^"]+)" already exists \((42710|42P07|42P06)\)/.exec(text);
1904
+ const bare = /(?:type|relation|constraint|schema) "([^"]+)" already exists/.exec(text);
1905
+ if ((withCode ?? bare)?.[1] === ATLAS_REVISIONS_TABLE) return null;
1571
1906
  if (withCode) return {
1572
1907
  object: withCode[1],
1573
1908
  code: withCode[2]
1574
1909
  };
1575
- const bare = /(?:type|relation|constraint|schema) "([^"]+)" already exists/.exec(text);
1576
- if (bare && /42710|42P07/.test(text)) return {
1910
+ const code = /42P07|42P06|42710/.exec(text)?.[0];
1911
+ if (bare && code) return {
1577
1912
  object: bare[1],
1578
- code: /42P07/.test(text) ? "42P07" : "42710"
1913
+ code
1579
1914
  };
1580
1915
  return null;
1581
1916
  }
1582
1917
  /**
1918
+ * The migration Atlas was running when it failed: `from version "<v>"` in
1919
+ * `sql/migrate: executing statement "…" from version "20261013201353": …`.
1920
+ */
1921
+ function parseFailedMigrationVersion(text) {
1922
+ return /from version "([^"]+)"/.exec(text)?.[1] ?? null;
1923
+ }
1924
+ /**
1583
1925
  * What to do about it: tell Atlas the database is already at a version.
1584
1926
  *
1585
1927
  * Atlas's own mechanism (`migrate apply --baseline <version>`), not a ledger of
1586
1928
  * ours — it writes the revision row Atlas reads, so every later `rebase db
1587
1929
  * migrate` is an ordinary one.
1588
1930
  */
1589
- function formatBaselineRemedy(version, object) {
1931
+ function formatBaselineRemedy(version, object, failedVersion) {
1590
1932
  const versionToken = version ?? "<version>";
1591
- return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ This database already has the schema this migration creates\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + (object ? ` ${chalk.gray(`"${object}" is already there, so the migration cannot create it.`)}\n\n` : "") + ` Nothing is wrong with the migration — the database was provisioned\n another way. Every Rebase boot ensures the schema, and ${chalk.bold("rebase db push")}\n applies it directly, so a database that has ever run either one is\n ahead of a migration history that was never recorded.\n\n Record where it already is, then migrate normally:\n\n ${chalk.bold.green(`rebase db migrate --baseline ${versionToken}`)}\n ${chalk.bold.green("rebase db migrate")}\n\n The baseline is the version already applied — the numeric prefix of\n the migration file describing what is in the database now. Migrations\n after it run; it and everything before it are marked done.\n\n On a database nothing has ever booted against, none of this is needed.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1933
+ const onlyTheFailedOne = failedVersion && failedVersion !== version ? ` To mark only the migration that failed, and run the ones after it:
1934
+
1935
+ ${chalk.bold.green(`rebase db migrate --baseline ${failedVersion}`)}\n\n` : "";
1936
+ return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ This database already has the schema this migration creates\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + (object ? ` ${chalk.gray(`"${object}" is already there, so the migration cannot create it.`)}\n\n` : "") + ` Nothing is wrong with the migration — the database was provisioned\n another way. Every Rebase boot ensures the schema, and ${chalk.bold("rebase db push")}\n applies it directly, so a database that has ever run either one is\n ahead of the migrations recorded on it.\n\n Record where it already is, then migrate normally:\n\n ${chalk.bold.green(`rebase db migrate --baseline ${versionToken}`)}\n ${chalk.bold.green("rebase db migrate")}\n\n The baseline is the version already applied — the numeric prefix of\n the migration file describing what is in the database now. Migrations\n after it run; it and everything before it are marked done.\n\n` + onlyTheFailedOne + " On a database nothing has ever booted against, none of this is needed.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
1937
+ }
1938
+ /**
1939
+ * Atlas's whole report when a connection was closed before it answered:
1940
+ * `Error: EOF`, sometimes with the step in front (`Error: postgres: querying
1941
+ * system variables: EOF`). The line names neither the database nor the
1942
+ * connection, so on its own it reads like a crash.
1943
+ */
1944
+ function parseConnectionEof(text) {
1945
+ return /^Error: (?:.*: )?EOF\s*$/m.test(text);
1946
+ }
1947
+ /**
1948
+ * What a closed connection means, and that running the command again is safe.
1949
+ *
1950
+ * On the managed development database it is almost always the connection
1951
+ * limit. PGlite is one session behind a socket server that accepts only a few
1952
+ * connections; a running `rebase dev` holds some, `atlas schema apply` opens
1953
+ * two, and the one over the limit is closed unanswered. Measured: with the
1954
+ * daemon's limit of four and three held, `db push --dry-run` failed with a
1955
+ * bare `Error: EOF`; with two held it passed. The dev server's pool lets an
1956
+ * idle connection go after 30 seconds (`idleTimeoutMillis` in connection.ts),
1957
+ * which is why a later attempt usually works.
1958
+ */
1959
+ function formatConnectionEofBanner(managed) {
1960
+ const why = managed ? ` The managed development database accepts only a few connections at a\n time, and a running ${chalk.bold("rebase dev")} holds some of them. Atlas opened one more,\n and it was closed before the database answered.\n` : " The server, or a proxy or pooler in front of it, closed the connection:\n a restart, a connection limit, or a reaper for idle connections.\n";
1961
+ const next = managed ? ` Run the command again in a moment: it is safe to repeat, and the dev
1962
+ server lets an idle connection go after 30 seconds. If it keeps failing,
1963
+ stop ${chalk.bold("rebase dev")} while you run it.\n` : ` Run the command again: it is safe to repeat. If it keeps failing, check\n the server's connection limit and that it is up: ${chalk.bold("rebase doctor")}.\n`;
1964
+ return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ Atlas lost its connection to the database (EOF)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + why + `\n` + next + "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
1592
1965
  }
1593
1966
  /** `ALTER TABLE … SET NOT NULL` on a table that already holds rows (23502). */
1594
1967
  function parseNotNullViolation(text) {
@@ -1895,9 +2268,10 @@ function formatForeignGeneratedColumnBanner(conflicts) {
1895
2268
  */
1896
2269
  async function diagnoseAtlasFailure(context) {
1897
2270
  const { domain, args, stderr, databaseUrl } = context;
2271
+ if (parseConnectionEof(stderr)) return formatConnectionEofBanner(context.managedDatabase === true);
1898
2272
  if (domain === "migrate" && args.includes("apply")) {
1899
2273
  const provisioned = parseAlreadyProvisioned(stderr);
1900
- if (provisioned) return formatBaselineRemedy(context.latestMigrationVersion ?? null, provisioned.object);
2274
+ if (provisioned) return formatBaselineRemedy(context.latestMigrationVersion ?? null, provisioned.object, parseFailedMigrationVersion(stderr));
1901
2275
  }
1902
2276
  if (domain === "schema" && args.includes("apply")) {
1903
2277
  const enumDrop = parseEnumLabelDrop(stderr);
@@ -1914,6 +2288,6 @@ async function diagnoseAtlasFailure(context) {
1914
2288
  return null;
1915
2289
  }
1916
2290
  //#endregion
1917
- export { queryColumnTypes as A, prepareGeneratedSqlDir as B, getForeignIndexExcludes as C, getVectorExcludes as D, getTriggerExcludes as E, resolveLocalBin as F, isSyncedPlan as G, detectDestructiveStatements as H, withAppClient as I, extractCauseMessage as K, DESIRED_STATE_URL as L, readSearchDdl as M, readTriggersDdl as N, loadCollectionsForCli as O, readVectorDdl as P, GENERATED_SQL_DIR as R, ensureDevDatabaseExists as S, getTableExcludes as T, extractPlanStatements as U, decidePushSafety as V, fromAlterTable as W, describeReinstallCommand as _, formatForeignGeneratedColumnBanner as a, dropDevDatabase as b, reportCommandFailure as c, applyTriggersDdl as d, applyVectorDdl as f, describeDevAddCommand as g, describeBuildScriptRemedy as h, diagnoseDbError as i, queryGeneratedColumnDependencies as j, promptConfirm as k, ExcludeIntrospectionError as l, closeAppClients as m, deepestErrorMessage as n, isEconnrefused as o, atlasDevDatabaseUrl as p, diagnoseAtlasFailure as r, parseHostInfo as s, checkDatabaseConnectivity as t, applySearchDdl as u, detectProjectPackageManager as v, getSearchExcludes as w, dropGeneratedColumns as x, diagnoseMissingBin as y, generatedSqlDir as z };
2291
+ export { redactConnectionPasswords as $, getVectorExcludes as A, workspaceMemberDir as B, dropGeneratedColumns as C, getTableExcludes as D, getSearchExcludes as E, readSearchDdl as F, decidePushSafety as G, GENERATED_SQL_DIR as H, readTriggersDdl as I, extractPlanStatements as J, destructiveOptInFlag as K, readVectorDdl as L, queryColumnTypes as M, queryGeneratedColumnDependencies as N, getTriggerExcludes as O, queryUndeclarableConstraints as P, forLibpq as Q, resolveLocalBin as R, dropDevDatabase as S, getForeignIndexExcludes as T, generatedSqlDir as U, DESIRED_STATE_URL as V, prepareGeneratedSqlDir as W, isSyncedPlan as X, fromAlterTable as Y, forAtlas as Z, describeBuildScriptRemedy as _, formatForeignGeneratedColumnBanner as a, detectProjectPackageManager as b, isSslNotEnabled as c, ExcludeIntrospectionError as d, splitPassword as et, applySearchDdl as f, closeAppClients as g, atlasDevDatabaseUrl as h, diagnoseDbError as i, loadCollectionsForCli as j, getUndeclaredConstraintExcludes as k, parseHostInfo as l, applyVectorDdl as m, deepestErrorMessage as n, isAuthFailure as o, applyTriggersDdl as p, detectDestructiveStatements as q, diagnoseAtlasFailure as r, isEconnrefused as s, checkDatabaseConnectivity as t, extractCauseMessage as tt, reportCommandFailure as u, describeDevAddCommand as v, ensureDevDatabaseExists as w, diagnoseMissingBin as x, describeReinstallCommand as y, withAppClient as z };
1918
2292
 
1919
- //# sourceMappingURL=cli-errors-CaI4ntyD.js.map
2293
+ //# sourceMappingURL=cli-errors-mx4sSjB8.js.map