@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.gcb918fc

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 (172) hide show
  1. package/README.md +16 -0
  2. package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
  3. package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
  4. package/dist/PostgresBackendDriver.d.ts +91 -2
  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 +6 -0
  8. package/dist/backup/backup-cli.d.ts +14 -0
  9. package/dist/backup/backup-service.d.ts +2 -2
  10. package/dist/backup/pg-binary.d.ts +5 -0
  11. package/dist/{backup-cli-DW5p9_zv.js → backup-cli-DnpJi6I7.js} +37 -29
  12. package/dist/backup-cli-DnpJi6I7.js.map +1 -0
  13. package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
  14. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  15. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  16. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  17. package/dist/cli-console-Ca6ZlAVU.js +154 -0
  18. package/dist/cli-console-Ca6ZlAVU.js.map +1 -0
  19. package/dist/cli-console.d.ts +39 -0
  20. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  21. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  22. package/dist/cli-errors.d.ts +51 -5
  23. package/dist/{cli-flags-BglvpjHv.js → cli-flags-JNI-pwSf.js} +6 -3
  24. package/dist/cli-flags-JNI-pwSf.js.map +1 -0
  25. package/dist/cli-helpers.d.ts +40 -0
  26. package/dist/cli-output.d.ts +16 -28
  27. package/dist/cli-push.d.ts +87 -0
  28. package/dist/cli.js +566 -182
  29. package/dist/cli.js.map +1 -1
  30. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
  31. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
  32. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C9mT8bgf.js} +24 -112
  33. package/dist/connection-C9mT8bgf.js.map +1 -0
  34. package/dist/connection.d.ts +13 -2
  35. package/dist/databasePoolManager-BIuewidb.js +119 -0
  36. package/dist/databasePoolManager-BIuewidb.js.map +1 -0
  37. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
  38. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  39. package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
  40. package/dist/doctor-C4WiftHm.js.map +1 -0
  41. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  42. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  43. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  44. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  45. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CVm_yvzb.js} +173 -77
  46. package/dist/ensure-tables-CVm_yvzb.js.map +1 -0
  47. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  48. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  49. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  50. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  51. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
  52. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  53. package/dist/generate-files-CdEckX6c.js +199 -0
  54. package/dist/generate-files-CdEckX6c.js.map +1 -0
  55. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
  56. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
  57. package/dist/generated-sql.d.ts +8 -2
  58. package/dist/history/HistoryService.d.ts +64 -5
  59. package/dist/history/ensure-history-table.d.ts +63 -4
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.es.js +139 -19236
  62. package/dist/index.es.js.map +1 -1
  63. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
  64. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  65. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
  66. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  67. package/dist/open-rebase-CbOur0TU.js +20858 -0
  68. package/dist/open-rebase-CbOur0TU.js.map +1 -0
  69. package/dist/open-rebase.d.ts +86 -0
  70. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  71. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  72. package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
  73. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  74. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
  75. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  76. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  77. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  78. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  79. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  80. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
  81. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  82. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
  83. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  84. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
  85. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  86. package/dist/schema/auth-schema.d.ts +2 -2
  87. package/dist/schema/boot-ddl.d.ts +47 -0
  88. package/dist/schema/collection-index.d.ts +45 -0
  89. package/dist/schema/column-plan-helpers.d.ts +5 -0
  90. package/dist/schema/deferred-index-build.d.ts +131 -0
  91. package/dist/schema/destructive-sql.d.ts +7 -0
  92. package/dist/schema/doctor-cli.js +110 -6
  93. package/dist/schema/doctor-cli.js.map +1 -1
  94. package/dist/schema/doctor-search-scale.d.ts +45 -0
  95. package/dist/schema/doctor.d.ts +20 -1
  96. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  97. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  98. package/dist/schema/enum-labels-kept.d.ts +26 -0
  99. package/dist/schema/generate-drizzle-schema.js +1 -1
  100. package/dist/schema/generate-files.d.ts +44 -0
  101. package/dist/schema/generate-next-step.d.ts +0 -13
  102. package/dist/schema/generate-postgres-ddl.js +10 -81
  103. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  104. package/dist/schema/introspect-db-logic.d.ts +12 -0
  105. package/dist/schema/introspect-db-queries.d.ts +11 -1
  106. package/dist/schema/introspect-db-structure.d.ts +10 -0
  107. package/dist/schema/introspect-db.js +4 -5
  108. package/dist/schema/introspect-db.js.map +1 -1
  109. package/dist/schema/migration-post-process.d.ts +34 -0
  110. package/dist/schema/plan/diff-plan.d.ts +126 -0
  111. package/dist/schema/plan/types.d.ts +8 -0
  112. package/dist/schema/push-index-prebuild.d.ts +25 -0
  113. package/dist/schema/push-legacy-renames.d.ts +20 -0
  114. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  115. package/dist/security/rls-enforcement.d.ts +32 -4
  116. package/dist/services/FetchService.d.ts +58 -8
  117. package/dist/services/PersistService.d.ts +15 -0
  118. package/dist/services/RelationService.d.ts +88 -3
  119. package/dist/services/cdc/CdcListener.d.ts +17 -6
  120. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  121. package/dist/services/channel-bus/index.d.ts +1 -1
  122. package/dist/services/channel-history.d.ts +15 -4
  123. package/dist/services/collection-helpers.d.ts +15 -0
  124. package/dist/services/field-viewer.d.ts +15 -1
  125. package/dist/services/pg-notify-listener.d.ts +7 -0
  126. package/dist/services/realtimeService.d.ts +314 -14
  127. package/dist/services/row-pipeline.d.ts +16 -9
  128. package/dist/services/socket-liveness.d.ts +42 -2
  129. package/dist/services/sql-script.d.ts +207 -3
  130. package/dist/services/vector-iterative-scan.d.ts +40 -0
  131. package/dist/services/write-depth.d.ts +6 -0
  132. package/dist/services/write-transaction-scope.d.ts +9 -0
  133. package/dist/socket-opt-ins.d.ts +64 -0
  134. package/dist/source-DBDbBTL4.js +811 -0
  135. package/dist/source-DBDbBTL4.js.map +1 -0
  136. package/dist/tsx-loader.d.ts +14 -0
  137. package/dist/utils/drizzle-conditions.d.ts +21 -1
  138. package/dist/utils/error-cause.d.ts +7 -0
  139. package/dist/utils/pg-error-utils.d.ts +2 -4
  140. package/dist/utils/result-rows.d.ts +10 -0
  141. package/dist/websocket-BLGc5yhs.js +1604 -0
  142. package/dist/websocket-BLGc5yhs.js.map +1 -0
  143. package/dist/websocket.d.ts +13 -0
  144. package/package.json +7 -7
  145. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  146. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  147. package/dist/cli-errors-DsA-K9uP.js +0 -893
  148. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  149. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  150. package/dist/cli-output-CNdMql-L.js +0 -47
  151. package/dist/cli-output-CNdMql-L.js.map +0 -1
  152. package/dist/collection-index-DdnsxiJ_.js +0 -286
  153. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  154. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  155. package/dist/doctor-C-sYWbmt.js.map +0 -1
  156. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  157. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  158. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  159. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  160. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  161. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  162. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  163. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  164. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  165. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  166. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  167. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  168. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  169. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  170. package/dist/source-Br7L7GOI.js +0 -410
  171. package/dist/source-Br7L7GOI.js.map +0 -1
  172. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -0,0 +1,1919 @@
1
+ import { createRequire as __createRequire } from "module";
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";
5
+ import path from "path";
6
+ import fs from "fs";
7
+ import { fileURLToPath, pathToFileURL } from "url";
8
+ import { execSync } from "child_process";
9
+ import { createRequire } from "module";
10
+ //#region src/utils/error-cause.ts
11
+ /**
12
+ * Walk the error cause chain and return the deepest meaningful message.
13
+ *
14
+ * A leaf module, imported by the driver CLI: `pg-error-utils.ts`, where it used
15
+ * to live, imports `@rebasepro/server`, and the CLI must start without it.
16
+ */
17
+ function extractCauseMessage(error) {
18
+ if (!error || typeof error !== "object") return null;
19
+ if (!(error instanceof Error)) return null;
20
+ if (error.cause && typeof error.cause === "object") {
21
+ const deeper = extractCauseMessage(error.cause);
22
+ if (deeper) return deeper;
23
+ if (error.cause instanceof Error && error.cause.message) return error.cause.message;
24
+ }
25
+ return null;
26
+ }
27
+ //#endregion
28
+ //#region src/schema/destructive-sql.ts
29
+ var import___vite_browser_external = /* @__PURE__ */ __toESM(require___vite_browser_external(), 1);
30
+ /**
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.
33
+ *
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.
40
+ *
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.
47
+ *
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.
51
+ *
52
+ * Everything in this file is side-effect free so it can be unit-tested
53
+ * without Atlas or a database.
54
+ */
55
+ /**
56
+ * SQL fragments that destroy data or data-bearing objects. Matched
57
+ * case-insensitively against each statement of the plan. `IF EXISTS` /
58
+ * whitespace variations are tolerated by the regexes below.
59
+ */
60
+ var DESTRUCTIVE_PATTERNS = [
61
+ {
62
+ label: "DROP TABLE",
63
+ re: /\bDROP\s+TABLE\b/i
64
+ },
65
+ {
66
+ label: "DROP COLUMN",
67
+ re: /\bDROP\s+COLUMN\b/i
68
+ },
69
+ {
70
+ label: "DROP SCHEMA",
71
+ re: /\bDROP\s+SCHEMA\b/i
72
+ },
73
+ {
74
+ label: "DROP VIEW",
75
+ re: /\bDROP\s+(MATERIALIZED\s+)?VIEW\b/i
76
+ },
77
+ {
78
+ label: "DROP TYPE",
79
+ re: /\bDROP\s+TYPE\b/i
80
+ },
81
+ {
82
+ label: "TRUNCATE",
83
+ re: /\bTRUNCATE\b/i
84
+ }
85
+ ];
86
+ /**
87
+ * Split a SQL script into individual statements, dropping blank lines and
88
+ * `--` comment lines. Deliberately simple: Atlas emits one plain statement
89
+ * per `;`, without string literals that contain semicolons in a schema DDL
90
+ * plan, so a naive split is safe and keeps this dependency-free.
91
+ */
92
+ function splitSqlStatements(sql) {
93
+ return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
94
+ }
95
+ /** `->`, the marker Atlas prints in front of the first line of every statement. */
96
+ var PLAN_MARKER_RE = /^\s*->\s?/;
97
+ /** `-------`, both the rule between sections and the one above a summary. */
98
+ var PLAN_RULE_RE = /^\s*-{5,}\s*$/;
99
+ /** `Planning migration statements (3 in total):` — and the count it announces. */
100
+ var PLAN_HEADING_RE = /^Planning migration statements \((\d+) in total\):/m;
101
+ /**
102
+ * What Atlas prints, and nothing else, when there is nothing to plan — from
103
+ * `schema apply`, or from `migrate apply` with no file pending.
104
+ */
105
+ var PLAN_SYNCED_RE = /^\s*(?:Schema is synced, no changes to be made|No migration files to execute)\s*$/;
106
+ /**
107
+ * Read the statements out of whatever Atlas printed.
108
+ *
109
+ * `atlas schema apply --dry-run` does not print SQL. It prints a report, and
110
+ * the same statements appear in it twice:
111
+ *
112
+ * Planning migration statements (2 in total):
113
+ *
114
+ * -- modify "people" table:
115
+ * -> ALTER TABLE "public"."people" ALTER COLUMN "rating" TYPE numeric(4,1);
116
+ * -- create "notes" table:
117
+ * -> CREATE TABLE "public"."notes" (
118
+ * "body" text NULL DEFAULT 'a;b',
119
+ * …
120
+ * );
121
+ *
122
+ * -------------------------------------------
123
+ *
124
+ * Analyzing planned statements (2 in total):
125
+ * …
126
+ * Running dry-run migration (2 statements in total):
127
+ * -- modify "people" table
128
+ * -> ALTER TABLE …; ← the same statement, again
129
+ * -- ok (19µs)
130
+ *
131
+ * Splitting that on `;` is how the push gate went blind: the heading has no
132
+ * `;` and does not start with `--`, so it became the first line of the first
133
+ * statement, and an `ALTER TABLE` parse anchored at the start of a statement
134
+ * read nothing — every narrowing type change applied without a prompt. Reading
135
+ * both sections was a second bug, counting every destructive statement twice.
136
+ *
137
+ * So the report is read as a report: a statement is a `->` line and the lines
138
+ * indented under it, up to the next `--` heading, `->`, blank line or rule;
139
+ * only the first section that holds a `->` is read; and `;` plays no part,
140
+ * because a default like `'a;b'` contains one.
141
+ *
142
+ * Plain SQL — a migration file, or an Atlas that printed SQL — has no `->`
143
+ * and is split the old way.
144
+ */
145
+ function readPlan(plan) {
146
+ if (PLAN_SYNCED_RE.test(plan)) return { statements: [] };
147
+ const lines = plan.split(/\r?\n/);
148
+ if (!lines.some((line) => PLAN_MARKER_RE.test(line))) return { statements: splitSqlStatements(plan) };
149
+ const heading = PLAN_HEADING_RE.exec(plan);
150
+ const statements = [];
151
+ let current = null;
152
+ const flush = () => {
153
+ if (current) statements.push(current.join("\n").trim().replace(/;\s*$/, "").trimEnd());
154
+ current = null;
155
+ };
156
+ for (const line of lines) {
157
+ if (PLAN_MARKER_RE.test(line)) {
158
+ flush();
159
+ current = [line.replace(PLAN_MARKER_RE, "")];
160
+ continue;
161
+ }
162
+ if (PLAN_RULE_RE.test(line) || /^\S/.test(line) && !line.startsWith("--")) {
163
+ flush();
164
+ if (statements.length > 0) break;
165
+ continue;
166
+ }
167
+ if (!current) continue;
168
+ if (line.trim() === "" || line.trim().startsWith("--")) {
169
+ flush();
170
+ continue;
171
+ }
172
+ current.push(line);
173
+ }
174
+ flush();
175
+ return {
176
+ statements,
177
+ announced: heading ? Number(heading[1]) : void 0
178
+ };
179
+ }
180
+ /**
181
+ * The statements of an Atlas plan, each exactly once, without Atlas's
182
+ * rendering and without the trailing `;`. See {@link readPlan} for the shape
183
+ * it reads; `test/fixtures/atlas-dry-run/` holds real captures of it.
184
+ */
185
+ function extractPlanStatements(plan) {
186
+ return readPlan(plan).statements;
187
+ }
188
+ /**
189
+ * Whether Atlas answered the dry-run with "Schema is synced" and nothing else.
190
+ *
191
+ * Positive evidence only: an empty or unreadable report also yields no
192
+ * statements, and is not the same answer.
193
+ */
194
+ function isSyncedPlan(plan) {
195
+ return PLAN_SYNCED_RE.test(plan);
196
+ }
197
+ /**
198
+ * Every column of every ordinary table outside the system schemas, with its
199
+ * type. What {@link detectDestructiveStatements} compares a planned type
200
+ * change against; `format_type` because it carries the modifier (`(255)`,
201
+ * `(10,2)`) that decides whether a change widens or narrows.
202
+ */
203
+ var COLUMN_TYPES_SQL = `
204
+ SELECT n.nspname AS "schema",
205
+ c.relname AS "table",
206
+ a.attname AS "column",
207
+ format_type(a.atttypid, a.atttypmod) AS "type"
208
+ FROM pg_attribute a
209
+ JOIN pg_class c ON c.oid = a.attrelid
210
+ JOIN pg_namespace n ON n.oid = c.relnamespace
211
+ WHERE c.relkind IN ('r', 'p')
212
+ AND a.attnum > 0
213
+ AND NOT a.attisdropped
214
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
215
+ AND n.nspname NOT LIKE 'pg\\_%'
216
+ `;
217
+ /**
218
+ * Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
219
+ * the statements that would destroy data. An empty array means the plan is
220
+ * safe to auto-approve.
221
+ *
222
+ * `columnTypes` is what the database holds now. A type change to a column it
223
+ * does not list is flagged: not knowing what a column is means not knowing
224
+ * whether the change keeps its values.
225
+ */
226
+ function detectDestructiveStatements(planSql, columnTypes = []) {
227
+ const found = [];
228
+ const { statements, announced } = readPlan(planSql);
229
+ if (announced !== void 0 && announced !== statements.length) found.push({
230
+ statement: `Planning migration statements (${announced} in total)`,
231
+ kind: "UNREADABLE PLAN",
232
+ detail: `Atlas announced ${announced} statement(s) and ${statements.length} could be read — read the plan yourself before allowing it`
233
+ });
234
+ for (const statement of statements) {
235
+ const dropped = DESTRUCTIVE_PATTERNS.find(({ re }) => re.test(statement));
236
+ const rekeyed = describePrimaryKeyChange(statement);
237
+ const lossy = parseColumnTypeChanges(statement).map((change) => ({
238
+ change,
239
+ from: currentTypeOf(change, columnTypes)
240
+ })).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);
242
+ if (!kind) continue;
243
+ const detail = [rekeyed, lossy].filter(Boolean).join("; ");
244
+ found.push(detail ? {
245
+ statement,
246
+ kind,
247
+ detail
248
+ } : {
249
+ statement,
250
+ kind
251
+ });
252
+ }
253
+ return found;
254
+ }
255
+ var IDENT = String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)`;
256
+ var ALTER_TABLE_RE = new RegExp(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?(${IDENT})(?:\.(${IDENT}))?\s+([\s\S]*)$`, "i");
257
+ var TYPE_CLAUSE_RE = new RegExp(String.raw`^ALTER\s+(?:COLUMN\s+)?(${IDENT})\s+(?:SET\s+DATA\s+)?TYPE\s+([\s\S]+)$`, "i");
258
+ var unquote = (ident) => ident.startsWith("\"") ? ident.slice(1, -1).replace(/""/g, "\"") : ident;
259
+ /**
260
+ * Split an `ALTER TABLE`'s clause list on its top-level commas — the ones
261
+ * outside parentheses and quotes, so `TYPE numeric(10, 2)` stays one clause.
262
+ */
263
+ function splitClauses(body) {
264
+ const clauses = [];
265
+ let depth = 0;
266
+ let quote;
267
+ let start = 0;
268
+ for (let i = 0; i < body.length; i++) {
269
+ const ch = body[i];
270
+ if (quote) {
271
+ if (ch === quote) quote = void 0;
272
+ } else if (ch === "\"" || ch === "'") quote = ch;
273
+ else if (ch === "(") depth++;
274
+ else if (ch === ")") depth--;
275
+ else if (ch === "," && depth === 0) {
276
+ clauses.push(body.slice(start, i).trim());
277
+ start = i + 1;
278
+ }
279
+ }
280
+ clauses.push(body.slice(start).trim());
281
+ return clauses.filter((clause) => clause.length > 0);
282
+ }
283
+ /**
284
+ * The statement from its `ALTER TABLE` on, wherever on a line that starts.
285
+ *
286
+ * {@link readPlan} hands over clean statements. This is the second line of
287
+ * defence for text it could not clean — a rendering it does not know yet —
288
+ * where a line of prose above the statement must not hide the statement, which
289
+ * is exactly how an anchored match once read Atlas's whole plan as safe.
290
+ */
291
+ function fromAlterTable(statement) {
292
+ const start = /(?:^|\n)[ \t]*(?:->[ \t]*)?(ALTER\s+TABLE\b)/i.exec(statement);
293
+ return start ? statement.slice(start.index + start[0].length - start[1].length).trim() : statement.trim();
294
+ }
295
+ /**
296
+ * Every `ALTER COLUMN … [SET DATA] TYPE` in one statement. Atlas prints its
297
+ * dry-run statements under a `-> ` marker and puts several clauses in one
298
+ * `ALTER TABLE`, so both are handled.
299
+ */
300
+ function parseColumnTypeChanges(statement) {
301
+ const table = ALTER_TABLE_RE.exec(fromAlterTable(statement));
302
+ if (!table) return [];
303
+ const [schema, name] = table[2] ? [unquote(table[1]), unquote(table[2])] : [void 0, unquote(table[1])];
304
+ const changes = [];
305
+ for (const clause of splitClauses(table[3])) {
306
+ const match = TYPE_CLAUSE_RE.exec(clause);
307
+ if (!match) continue;
308
+ const to = match[2].split(/\s+(?:USING|COLLATE)\s/i)[0].trim();
309
+ changes.push({
310
+ schema,
311
+ table: name,
312
+ column: unquote(match[1]),
313
+ to
314
+ });
315
+ }
316
+ return changes;
317
+ }
318
+ /** `ADD [CONSTRAINT "x"] PRIMARY KEY ("a", "b")`, one clause of an `ALTER TABLE`. */
319
+ var ADD_PRIMARY_KEY_RE = new RegExp(String.raw`^ADD\s+(?:CONSTRAINT\s+${IDENT}\s+)?PRIMARY\s+KEY\b\s*(?:\(([^)]*)\))?`, "i");
320
+ /** `DROP CONSTRAINT [IF EXISTS] "x"`, one clause of an `ALTER TABLE`. */
321
+ var DROP_CONSTRAINT_RE = new RegExp(String.raw`^DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?(${IDENT})`, "i");
322
+ /** What a row loses when the columns that address it change. */
323
+ var ROW_ADDRESS_LOST = "every row's id changes, so anything that refers to a row by its id — a URL, a foreign key, an id stored elsewhere — stops finding it";
324
+ /**
325
+ * What an `ALTER TABLE` does to the table's primary key, in a sentence — or
326
+ * `undefined` when it leaves the key alone.
327
+ *
328
+ * The key is a row's address: Rebase reads, links and routes every row by it.
329
+ * Atlas plans a key change as `DROP CONSTRAINT "<table>_pkey", ADD PRIMARY KEY
330
+ * (…)` — one routine-looking statement that names no DROP of data — so moving
331
+ * a collection from `(id)` to `(id, locale)` re-keyed a populated table with
332
+ * no prompt. A `CREATE TABLE`'s own `PRIMARY KEY` is not a change and is never
333
+ * read here: only an `ALTER TABLE`'s clauses are. A dropped constraint counts
334
+ * as the key when it carries Postgres's default name for one, `<table>_pkey`;
335
+ * a key under another name is caught by the `ADD PRIMARY KEY` that replaces it.
336
+ */
337
+ function describePrimaryKeyChange(statement) {
338
+ const alter = ALTER_TABLE_RE.exec(fromAlterTable(statement));
339
+ if (!alter) return void 0;
340
+ const table = alter[2] ? `${alter[1]}.${alter[2]}` : alter[1];
341
+ let added;
342
+ let dropped;
343
+ for (const clause of splitClauses(alter[3])) {
344
+ const add = ADD_PRIMARY_KEY_RE.exec(clause);
345
+ if (add) {
346
+ added = add[1] === void 0 ? "a new key" : `(${add[1].split(",").map((c) => c.trim()).join(", ")})`;
347
+ continue;
348
+ }
349
+ const drop = DROP_CONSTRAINT_RE.exec(clause);
350
+ if (drop && unquote(drop[1]).endsWith("_pkey")) dropped = drop[1];
351
+ }
352
+ if (added && dropped) return `${table} is re-keyed on ${added}: ${ROW_ADDRESS_LOST}`;
353
+ if (added) return `${table} gets the primary key ${added}: rows are addressed by it from now on, and the push fails if two rows share a value`;
354
+ if (dropped) return `${table} loses its primary key ${dropped}: ${ROW_ADDRESS_LOST}`;
355
+ }
356
+ /**
357
+ * The column's current type. A table named without its schema matches in any
358
+ * schema, but only when every match agrees — two same-named tables with
359
+ * different types leave the answer unknown.
360
+ */
361
+ function currentTypeOf(change, columnTypes) {
362
+ const types = new Set(columnTypes.filter((col) => col.table === change.table && col.column === change.column && (change.schema === void 0 || col.schema === change.schema)).map((col) => col.type));
363
+ return types.size === 1 ? [...types][0] : void 0;
364
+ }
365
+ /** Spellings of one type, mapped to one name. Anything absent compares verbatim. */
366
+ var TYPE_ALIASES = {
367
+ "int": "integer",
368
+ "int4": "integer",
369
+ "int2": "smallint",
370
+ "int8": "bigint",
371
+ "character varying": "varchar",
372
+ "decimal": "numeric",
373
+ "timestamp without time zone": "timestamp",
374
+ "timestamp with time zone": "timestamptz",
375
+ "time without time zone": "time",
376
+ "time with time zone": "timetz",
377
+ "bool": "boolean",
378
+ "float4": "real",
379
+ "float8": "double precision",
380
+ "character": "bpchar",
381
+ "char": "bpchar"
382
+ };
383
+ /** Decimal digits each integer type holds, which is what a `numeric` must hold too. */
384
+ var INTEGER_DIGITS = {
385
+ smallint: 5,
386
+ integer: 10,
387
+ bigint: 19
388
+ };
389
+ /** Types whose modifier is a fractional-seconds precision, 6 when absent. */
390
+ var SECONDS_PRECISION = /* @__PURE__ */ new Set([
391
+ "timestamp",
392
+ "timestamptz",
393
+ "time",
394
+ "timetz"
395
+ ]);
396
+ function parseType(raw) {
397
+ let text = raw.trim().replace(/\s+/g, " ");
398
+ let dims = 0;
399
+ for (let suffix = /\s*\[\d*\]$/.exec(text); suffix; suffix = /\s*\[\d*\]$/.exec(text)) {
400
+ dims++;
401
+ text = text.slice(0, suffix.index);
402
+ }
403
+ if (text.includes("\"")) return {
404
+ base: text,
405
+ dims
406
+ };
407
+ text = text.toLowerCase().replace(/^pg_catalog\./, "");
408
+ let args;
409
+ const modifier = /\(([^)]*)\)/.exec(text);
410
+ if (modifier) {
411
+ args = modifier[1].split(",").map((part) => Number(part.trim()));
412
+ text = `${text.slice(0, modifier.index)} ${text.slice(modifier.index + modifier[0].length)}`.replace(/\s+/g, " ").trim();
413
+ }
414
+ return {
415
+ base: TYPE_ALIASES[text] ?? text,
416
+ args,
417
+ dims
418
+ };
419
+ }
420
+ /** Does moving from one modifier to another, on the same type, keep every value? */
421
+ function modifierWidens(base, from, to) {
422
+ if (JSON.stringify(from) === JSON.stringify(to)) return true;
423
+ if (base === "varchar") return to === void 0 || from !== void 0 && to[0] >= from[0];
424
+ if (base === "numeric") {
425
+ if (to === void 0) return true;
426
+ if (from === void 0) return false;
427
+ const [p1, s1 = 0] = from;
428
+ const [p2, s2 = 0] = to;
429
+ return s2 >= s1 && p2 - s2 >= p1 - s1;
430
+ }
431
+ if (SECONDS_PRECISION.has(base)) return (to?.[0] ?? 6) >= (from?.[0] ?? 6);
432
+ return false;
433
+ }
434
+ /**
435
+ * Is `ALTER COLUMN … TYPE to` a widening, one that keeps every value exactly
436
+ * as it was?
437
+ *
438
+ * Deliberately short. Everything not listed needs a confirmation, including
439
+ * changes that are usually harmless: `timestamp` → `timestamptz` reads each
440
+ * value in the session's time zone, `json` → `jsonb` drops duplicate keys,
441
+ * `real` → `double precision` prints 0.1 as 0.100000001490116, and `integer` →
442
+ * `text` keeps the digits but changes what the column is.
443
+ *
444
+ * - the same type, with a modifier at least as wide (`varchar(100)` →
445
+ * `varchar(255)`, `numeric(10,2)` → `numeric(12,2)` or unbounded,
446
+ * `timestamptz(3)` → `timestamptz`);
447
+ * - `smallint` → `integer` → `bigint`, and any of them to a `numeric` with
448
+ * room for all their digits;
449
+ * - `varchar(n)` → `text`, and `text` → an unbounded `varchar`.
450
+ *
451
+ * Arrays follow their element type, and never gain or lose a dimension.
452
+ */
453
+ function isLosslessTypeChange(from, to) {
454
+ const a = parseType(from);
455
+ const b = parseType(to);
456
+ if (a.dims !== b.dims) return false;
457
+ if (a.args?.some(Number.isNaN) || b.args?.some(Number.isNaN)) return false;
458
+ if (a.base === b.base) return modifierWidens(a.base, a.args, b.args);
459
+ const fromDigits = INTEGER_DIGITS[a.base];
460
+ if (fromDigits !== void 0) {
461
+ const toDigits = INTEGER_DIGITS[b.base];
462
+ if (toDigits !== void 0) return toDigits >= fromDigits;
463
+ if (b.base === "numeric") return b.args === void 0 || b.args[0] - (b.args[1] ?? 0) >= fromDigits;
464
+ return false;
465
+ }
466
+ if (a.base === "varchar" && b.base === "text") return true;
467
+ if (a.base === "text" && b.base === "varchar") return b.args === void 0;
468
+ return false;
469
+ }
470
+ /**
471
+ * Decide how `db push` should proceed given the plan's destructiveness and
472
+ * the invocation context.
473
+ *
474
+ * - No destructive statements → `apply` (safe to auto-approve).
475
+ * - Destructive + `--allow-destructive` → `apply` (operator opted in).
476
+ * - Destructive + interactive TTY → `confirm` (prompt before applying).
477
+ * - Destructive + non-interactive → `refuse` (never silently drop data in
478
+ * CI / scripts / agents).
479
+ */
480
+ function decidePushSafety(opts) {
481
+ if (opts.destructiveCount === 0) return "apply";
482
+ if (opts.allowDestructive) return "apply";
483
+ return opts.interactive ? "confirm" : "refuse";
484
+ }
485
+ //#endregion
486
+ //#region src/module-dir.ts
487
+ var moduleDir = path.dirname(fileURLToPath(import.meta.url));
488
+ //#endregion
489
+ //#region src/generated-sql.ts
490
+ /**
491
+ * Where `db push` and `db generate` put the SQL they render from the collections.
492
+ *
493
+ * Five files — `schema.sql` (Atlas's desired state), `policies.sql`,
494
+ * `search.sql`, `vector.sql` and `triggers.sql` — and every one of them is
495
+ * scratch. Both commands regenerate all five before reading any of them, and
496
+ * nothing else reads them at all. They used to be written into `drizzle/`, beside
497
+ * the migrations, and committed: a copy nobody read, which every collection
498
+ * change rewrote in the diff and every two branches touching collections
499
+ * conflicted on.
500
+ *
501
+ * Relative to the directory the command runs in (the backend), like the
502
+ * migrations are.
503
+ */
504
+ /** The directory, relative to the working directory. POSIX, because it is also a URL path. */
505
+ var GENERATED_SQL_DIR = ".rebase/sql";
506
+ /**
507
+ * `schema.sql` as Atlas takes it. Relative on purpose, like the
508
+ * `file://drizzle/migrations` beside it: Atlas joins the URL's host and path, so
509
+ * `.rebase` is read as the first path segment, measured against the pinned 1.2.3.
510
+ */
511
+ var DESIRED_STATE_URL = `file://${GENERATED_SQL_DIR}/schema.sql`;
512
+ /** The files the DDL generator writes, all of them derived from the collections. */
513
+ var GENERATED_SQL_FILES = [
514
+ "schema.sql",
515
+ "policies.sql",
516
+ "search.sql",
517
+ "vector.sql",
518
+ "triggers.sql"
519
+ ];
520
+ /** The absolute directory, for the working directory the command runs in. */
521
+ function generatedSqlDir(cwd = process.cwd()) {
522
+ return path.resolve(cwd, GENERATED_SQL_DIR);
523
+ }
524
+ /**
525
+ * The first line of every file the generator writes, in every release that
526
+ * wrote them into `drizzle/`. Matching it is what makes removing the old copy
527
+ * safe: a file there under one of these names that does not open this way was
528
+ * written by a person, and is left alone.
529
+ */
530
+ var GENERATED_HEADER = /^-- This file (is auto-generated by the Rebase DDL generator|contains RLS policies generated by Rebase)\./;
531
+ /**
532
+ * Get the directory ready to be written, and retire the copies an older release
533
+ * committed.
534
+ *
535
+ * The directory ignores itself, rather than relying on the project's
536
+ * `.gitignore`: a scaffold ignores `.rebase/`, but a project created before it
537
+ * did would otherwise start committing these files from their new home.
538
+ *
539
+ * The old copies in `drizzle/` are deleted, not left behind. Nothing writes them
540
+ * any more, so from the first push after an upgrade they only fall further
541
+ * behind — and a stale `policies.sql` in the repository is exactly what someone
542
+ * reviewing the project's RLS would read.
543
+ *
544
+ * With `retire: false` (`db push --dry-run`, which writes nothing of the
545
+ * project's) the old copies are only found, not removed.
546
+ *
547
+ * @returns the old files it removed — or, with `retire: false`, would remove —
548
+ * relative to the working directory.
549
+ */
550
+ function prepareGeneratedSqlDir(cwd = process.cwd(), options = {}) {
551
+ const dir = generatedSqlDir(cwd);
552
+ fs.mkdirSync(dir, { recursive: true });
553
+ const ignore = path.join(dir, ".gitignore");
554
+ if (!fs.existsSync(ignore)) fs.writeFileSync(ignore, "# Generated by `rebase db push` / `rebase db generate` from the collections.\n*\n");
555
+ const removed = [];
556
+ for (const name of GENERATED_SQL_FILES) {
557
+ const legacy = path.join(cwd, "drizzle", name);
558
+ if (!fs.existsSync(legacy)) continue;
559
+ const firstLine = fs.readFileSync(legacy, "utf-8").split("\n", 1)[0];
560
+ if (!GENERATED_HEADER.test(firstLine)) continue;
561
+ if (options.retire !== false) fs.rmSync(legacy);
562
+ removed.push(path.posix.join("drizzle", name));
563
+ }
564
+ return removed;
565
+ }
566
+ //#endregion
567
+ //#region src/cli-helpers.ts
568
+ /**
569
+ * Which package manager installed this project, read off its lockfile.
570
+ *
571
+ * Every remedy for a blocked build script is package-manager specific, and
572
+ * naming the wrong one is the same failure as naming none: `pnpm approve-builds`
573
+ * is not a thing an npm user can run, and `pnpm.onlyBuiltDependencies` is a key
574
+ * npm does not read. This existed as a pnpm-only message for long enough that
575
+ * npm 12 — which blocks dependency lifecycle scripts by default, the way pnpm 10
576
+ * does — could ship and leave every npm reader three unfollowable commands deep.
577
+ *
578
+ * The lockfile, not `npm_config_user_agent`: the question is how the project's
579
+ * `node_modules` was built, not which binary happens to be invoking us. Walks up
580
+ * because commands run from `backend/` in a scaffolded project, and the lockfile
581
+ * is at the workspace root.
582
+ */
583
+ function detectProjectPackageManager(startDir = process.cwd()) {
584
+ const lockfiles = [
585
+ ["pnpm-lock.yaml", "pnpm"],
586
+ ["bun.lock", "bun"],
587
+ ["bun.lockb", "bun"],
588
+ ["yarn.lock", "yarn"],
589
+ ["package-lock.json", "npm"]
590
+ ];
591
+ let dir = path.resolve(startDir);
592
+ while (true) {
593
+ for (const [file, manager] of lockfiles) if (fs.existsSync(path.join(dir, file))) return manager;
594
+ const parent = path.dirname(dir);
595
+ if (parent === dir) return "unknown";
596
+ dir = parent;
597
+ }
598
+ }
599
+ /**
600
+ * How to let `@ariga/atlas` run its `preinstall`, in the reader's own package
601
+ * manager.
602
+ *
603
+ * Returns the lines to print after "the binary is missing but the package is
604
+ * installed". Kept next to {@link detectProjectPackageManager} so the two move
605
+ * together, and returned rather than printed so it can be asserted on.
606
+ */
607
+ function describeBuildScriptRemedy(packageName, manager = detectProjectPackageManager()) {
608
+ switch (manager) {
609
+ case "npm": return [
610
+ ` npm install-scripts approve ${packageName}`,
611
+ "",
612
+ " or, to record it in the project (what `rebase init` scaffolds):",
613
+ "",
614
+ " // package.json",
615
+ ` "allowScripts": { "${packageName}": true }`,
616
+ "",
617
+ " Then re-run `npm install`."
618
+ ];
619
+ case "yarn": return [
620
+ " // .yarnrc.yml",
621
+ " enableScripts: true",
622
+ "",
623
+ " Then re-run `yarn install`."
624
+ ];
625
+ case "bun": return [
626
+ " // package.json",
627
+ ` "trustedDependencies": ["${packageName}"]`,
628
+ "",
629
+ " Then re-run `bun install`."
630
+ ];
631
+ default: return [
632
+ " pnpm approve-builds",
633
+ "",
634
+ " or, to record it in the project (what `rebase init` scaffolds):",
635
+ "",
636
+ " // package.json",
637
+ ` "pnpm": { "onlyBuiltDependencies": ["${packageName}"] }`,
638
+ "",
639
+ " Then re-run `pnpm install`."
640
+ ];
641
+ }
642
+ }
643
+ /**
644
+ * "Install again, from scratch", in the reader's own package manager.
645
+ *
646
+ * For the `bin-link-missing` state: the tree is half-written, and re-resolving
647
+ * it is what recreates the `.bin` shim. `--force` on pnpm is what makes it
648
+ * relink rather than decide the tree is already up to date.
649
+ */
650
+ function describeReinstallCommand(manager = detectProjectPackageManager()) {
651
+ switch (manager) {
652
+ case "npm": return "rm -rf node_modules && npm install";
653
+ case "yarn": return "yarn install --force";
654
+ case "bun": return "rm -rf node_modules && bun install";
655
+ default: return "pnpm install --force";
656
+ }
657
+ }
658
+ /** `add <pkg>` as a dev dependency, in the reader's own package manager. */
659
+ function describeDevAddCommand(packageName, manager = detectProjectPackageManager()) {
660
+ 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}`;
665
+ }
666
+ }
667
+ /**
668
+ * Where a package's own manifest lives, or null if it is not installed here.
669
+ *
670
+ * `package.json` rather than the package root: a package with an `exports` map
671
+ * that omits `.` is unresolvable by name even when it is perfectly installed,
672
+ * which would misreport it as absent.
673
+ */
674
+ function resolvePackageManifest(packageName) {
675
+ const bases = [pathToFileURL(path.join(process.cwd(), "package.json")), pathToFileURL(path.join(moduleDir, "package.json"))];
676
+ for (const base of bases) try {
677
+ return createRequire(base).resolve(`${packageName}/package.json`);
678
+ } catch {}
679
+ return null;
680
+ }
681
+ /**
682
+ * The binaries a package's own manifest declares, as absolute paths.
683
+ *
684
+ * `bin` is a string for a single binary named after the package, or a map.
685
+ * Both forms appear in the wild and `@ariga/atlas` uses the map.
686
+ */
687
+ function declaredBinaries(manifestPath) {
688
+ let manifest;
689
+ try {
690
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
691
+ } catch {
692
+ return [];
693
+ }
694
+ const dir = path.dirname(manifestPath);
695
+ if (typeof manifest.bin === "string") return [path.resolve(dir, manifest.bin)];
696
+ if (manifest.bin && typeof manifest.bin === "object") return Object.values(manifest.bin).filter((target) => typeof target === "string").map((target) => path.resolve(dir, target));
697
+ return [];
698
+ }
699
+ function diagnoseMissingBin(packageName) {
700
+ const manifestPath = resolvePackageManifest(packageName);
701
+ if (!manifestPath) return "not-installed";
702
+ const binaries = declaredBinaries(manifestPath);
703
+ if (binaries.length === 0) return "build-script-blocked";
704
+ return binaries.some((file) => fs.existsSync(file)) ? "bin-link-missing" : "build-script-blocked";
705
+ }
706
+ function resolveLocalBin(binName) {
707
+ let dir = moduleDir;
708
+ while (true) {
709
+ const candidate = path.join(dir, "node_modules", ".bin", binName);
710
+ if (fs.existsSync(candidate)) return candidate;
711
+ const parent = path.dirname(dir);
712
+ if (parent === dir) break;
713
+ dir = parent;
714
+ }
715
+ let cwd = process.cwd();
716
+ while (true) {
717
+ const candidate = path.join(cwd, "node_modules", ".bin", binName);
718
+ if (fs.existsSync(candidate)) return candidate;
719
+ const parent = path.dirname(cwd);
720
+ if (parent === cwd) break;
721
+ cwd = parent;
722
+ }
723
+ try {
724
+ const globalPath = execSync(process.platform === "win32" ? `where ${binName}` : `which ${binName}`, { encoding: "utf-8" }).trim().split("\n")[0].trim();
725
+ if (globalPath && fs.existsSync(globalPath)) return globalPath;
726
+ } catch {}
727
+ return null;
728
+ }
729
+ async function getTableIncludesFromCollections(allCollections) {
730
+ const { getTableName, relationalCollections, resolveJunctionSpecs } = await import("@rebasepro/common");
731
+ const { isPostgresCollectionConfig } = await import("@rebasepro/types");
732
+ const collections = relationalCollections(allCollections);
733
+ const includes = [];
734
+ for (const col of collections) {
735
+ const tableName = getTableName(col);
736
+ const schema = isPostgresCollectionConfig(col) && col.schema ? col.schema : "public";
737
+ if (tableName) includes.push(`${schema}.${tableName}`);
738
+ }
739
+ for (const junction of resolveJunctionSpecs(collections).values()) includes.push(`${junction.schema}.${junction.table}`);
740
+ return Array.from(new Set(includes));
741
+ }
742
+ async function getTableIncludes(collectionsPath) {
743
+ return getTableIncludesFromCollections(await loadCollectionsForCli(collectionsPath));
744
+ }
745
+ /**
746
+ * Load a project's collections the way the Atlas-facing commands need them.
747
+ *
748
+ * Deliberately forgiving — a file that fails to import is skipped rather than
749
+ * fatal — because the callers use this to *narrow* what Atlas may touch, and a
750
+ * hard failure here would block a push over an unrelated broken file. Callers
751
+ * that cannot tolerate a partial answer (the table excludes, which fail closed)
752
+ * check the result themselves.
753
+ */
754
+ /** Paths this process has already complained about. See {@link loadCollectionsForCli}. */
755
+ var warnedMissingCollectionsPaths = /* @__PURE__ */ new Set();
756
+ async function loadCollectionsForCli(collectionsPath) {
757
+ const resolvedPath = path.resolve(collectionsPath);
758
+ const collections = [];
759
+ if (!fs.existsSync(resolvedPath) && !warnedMissingCollectionsPaths.has(resolvedPath)) {
760
+ warnedMissingCollectionsPaths.add(resolvedPath);
761
+ outWarn(chalk.yellow(` ⚠ Collections path not found: "${collectionsPath}"\n Resolved to: ${resolvedPath}\n (relative to cwd: ${process.cwd()})\n Pass an absolute path, or a path relative to where you run the command.`));
762
+ }
763
+ if (fs.existsSync(resolvedPath)) {
764
+ if (fs.statSync(resolvedPath).isDirectory()) {
765
+ const files = fs.readdirSync(resolvedPath);
766
+ for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") try {
767
+ const module = await import(pathToFileURL(path.join(resolvedPath, file)).href);
768
+ if (module && module.default) collections.push(module.default);
769
+ } catch {}
770
+ }
771
+ }
772
+ return collections;
773
+ }
774
+ /**
775
+ * The app database connections this process has open, by URL — one each.
776
+ *
777
+ * `rebase db push` used to open a connection per step: the auth bootstrap, the
778
+ * connectivity probe, each catalogue read behind the Atlas excludes, the
779
+ * column types, the generated-column edges, and then every post-step — RLS
780
+ * lock, auth tables, vector, search, triggers, policies, reconcile, role,
781
+ * legacy schema. 18 of them on a push that changed nothing. Locally each costs
782
+ * a few milliseconds; against a hosted Postgres over TLS a connect is four or
783
+ * five round trips (TCP, TLS 1.3, startup, SCRAM twice) — about 4 s of every
784
+ * push at 40 ms, and more on a serverless database that wakes per connection.
785
+ *
786
+ * Opened on first use and closed by {@link closeAppClients} when the command
787
+ * ends. The scratch database Atlas plans in is created and dropped through the
788
+ * `postgres` database, on a connection of its own: it is not this one.
789
+ */
790
+ var appClients = /* @__PURE__ */ new Map();
791
+ async function openAppClient(databaseUrl, connectionTimeoutMillis) {
792
+ const open = appClients.get(databaseUrl);
793
+ if (open) return open;
794
+ const opening = (async () => {
795
+ const { Client } = await import("pg");
796
+ const client = new Client({
797
+ connectionString: databaseUrl,
798
+ ...connectionTimeoutMillis ? { connectionTimeoutMillis } : {}
799
+ });
800
+ const forget = () => {
801
+ if (appClients.get(databaseUrl) === opening) appClients.delete(databaseUrl);
802
+ };
803
+ client.on("error", forget);
804
+ client.on("end", forget);
805
+ await client.connect();
806
+ return client;
807
+ })();
808
+ appClients.set(databaseUrl, opening);
809
+ try {
810
+ return await opening;
811
+ } catch (err) {
812
+ appClients.delete(databaseUrl);
813
+ throw err;
814
+ }
815
+ }
816
+ /**
817
+ * Run one step on this process's connection to the app database.
818
+ *
819
+ * Every step leaves the session as it found it, which is what a connection of
820
+ * its own used to guarantee by being closed. None of them sets anything for
821
+ * the session — `SET LOCAL` inside their own transaction, or `RESET` — and one
822
+ * that fails part-way through a transaction it opened is rolled back here, so
823
+ * the next step does not meet `current transaction is aborted`.
824
+ *
825
+ * @param options.connectionTimeoutMillis applies only when this call is the one
826
+ * that connects.
827
+ */
828
+ async function withAppClient(databaseUrl, step, options = {}) {
829
+ const client = await openAppClient(databaseUrl, options.connectionTimeoutMillis);
830
+ try {
831
+ return await step(client);
832
+ } catch (err) {
833
+ await client.query("ROLLBACK").catch(() => void 0);
834
+ throw err;
835
+ }
836
+ }
837
+ /** Close every connection {@link withAppClient} opened. Called once, as the command ends. */
838
+ async function closeAppClients() {
839
+ const open = [...appClients.values()];
840
+ appClients.clear();
841
+ await Promise.all(open.map(async (opening) => {
842
+ try {
843
+ await (await opening).end();
844
+ } catch {}
845
+ }));
846
+ }
847
+ /**
848
+ * Names the empty database Atlas plans against, instead of `<db>_dev_diff`.
849
+ *
850
+ * The derived one is made with `CREATE DATABASE` beside the real database,
851
+ * which assumes a server that holds more than one. PGlite — the managed
852
+ * development database — serves exactly one: the CREATE succeeds and creates
853
+ * nothing, so Atlas connected straight back to the database it was comparing
854
+ * and refused it as "not clean". `rebase db push` on the managed database now
855
+ * starts a second, throwaway PGlite and names it here. Anyone can name their
856
+ * own the same way; a named database is neither created nor dropped.
857
+ */
858
+ var ATLAS_DEV_URL_ENV = "REBASE_ATLAS_DEV_URL";
859
+ /**
860
+ * The database Atlas replays the desired schema into: the one
861
+ * {@link ATLAS_DEV_URL_ENV} names (`named: true` — not ours to create or
862
+ * drop), else `<db>_dev_diff`.
863
+ */
864
+ function atlasDevDatabaseUrl(databaseUrl, env) {
865
+ const named = env[ATLAS_DEV_URL_ENV]?.trim();
866
+ if (!named) return {
867
+ url: getDevDatabaseUrl(databaseUrl),
868
+ named: false
869
+ };
870
+ if (named === databaseUrl.trim()) throw new Error(`${ATLAS_DEV_URL_ENV} names the same database as DATABASE_URL. It must be a separate, empty database that Atlas may fill and clear.`);
871
+ return {
872
+ url: named,
873
+ named: true
874
+ };
875
+ }
876
+ function getDevDatabaseUrl(databaseUrl) {
877
+ try {
878
+ const parsed = new URL(databaseUrl);
879
+ parsed.pathname = `/${parsed.pathname.slice(1)}_dev_diff`;
880
+ return parsed.toString();
881
+ } catch {
882
+ return databaseUrl + "_dev_diff";
883
+ }
884
+ }
885
+ /**
886
+ * Create the scratch database Atlas plans against, and say so when we cannot.
887
+ *
888
+ * Atlas needs a `--dev-url` — an empty database it can build the desired state
889
+ * in — and this makes `<db>_dev_diff` next to the real one. The whole body used
890
+ * to be wrapped in an empty catch commented "Ignore, let Atlas handle
891
+ * connection failures", and Atlas's version of handling it is `postgres:
892
+ * querying system variables: pq: database "app_dev_diff" does not exist
893
+ * (3D000)` — the symptom, four frames downstream of the cause, with the cause
894
+ * thrown away.
895
+ *
896
+ * The cause is almost always `42501`: `CREATE DATABASE` is a privilege managed
897
+ * providers do not grant, so this is the first thing a hosted user hits and the
898
+ * one they can do least with.
899
+ *
900
+ * @returns whether the scratch database was created by *this* call — the only
901
+ * case in which dropping it afterwards is ours to do.
902
+ */
903
+ async function ensureDevDatabaseExists(databaseUrl, devDatabaseUrl) {
904
+ let devDbName = "";
905
+ try {
906
+ const { Client } = await import("pg");
907
+ const parsed = new URL(databaseUrl);
908
+ devDbName = new URL(devDatabaseUrl).pathname.slice(1);
909
+ parsed.pathname = "/postgres";
910
+ const client = new Client({ connectionString: parsed.toString() });
911
+ await client.connect();
912
+ try {
913
+ if ((await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [devDbName])).rowCount === 0) {
914
+ await client.query(`CREATE DATABASE "${devDbName}"`);
915
+ out(chalk.gray(` ✓ Created validation database "${devDbName}"`));
916
+ return true;
917
+ }
918
+ } finally {
919
+ await client.end();
920
+ }
921
+ } catch (err) {
922
+ outWarn(describeDevDatabaseFailure(err, devDbName || "<db>_dev_diff"));
923
+ }
924
+ return false;
925
+ }
926
+ /**
927
+ * What went wrong creating the scratch database, and what to do about it.
928
+ *
929
+ * Exported for its test: the `42501` branch is the one that matters and it
930
+ * cannot be provoked without a non-superuser role.
931
+ */
932
+ function describeDevDatabaseFailure(err, devDbName) {
933
+ const code = err?.code;
934
+ const message = err instanceof Error ? err.message : String(err);
935
+ const lines = [chalk.yellow(` ⚠ Could not create the Atlas scratch database "${devDbName}": ${message}`)];
936
+ if (code === "42501") lines.push(chalk.gray(` Atlas plans every change in a throwaway copy of the schema, and CREATE DATABASE
937
+ is a privilege most managed providers withhold. Two ways forward:
938
+ • GRANT the role CREATEDB: ALTER ROLE "<role>" CREATEDB;
939
+ • or create it once by hand: CREATE DATABASE "${devDbName}";`));
940
+ else lines.push(chalk.gray(" Atlas will fail next with `database \"" + devDbName + "\" does not exist (3D000)`, which is this."));
941
+ return lines.join("\n");
942
+ }
943
+ /**
944
+ * Drop the scratch database again, after the command that needed it succeeded.
945
+ *
946
+ * It is a full copy of the schema — no rows, but every table, type and index —
947
+ * and it was left behind per target forever. The only notice anyone got was
948
+ * `rebase db branch prune` reporting "3 Atlas scratch database(s) left over
949
+ * from db push", and on a provider that bills per database it is a silent cost.
950
+ *
951
+ * Deliberately NOT called on the failure path: after a failed push the scratch
952
+ * database holds the state Atlas was planning against, which is the one thing
953
+ * worth inspecting.
954
+ */
955
+ async function dropDevDatabase(databaseUrl, devDatabaseUrl) {
956
+ try {
957
+ const { Client } = await import("pg");
958
+ const parsed = new URL(databaseUrl);
959
+ const devDbName = new URL(devDatabaseUrl).pathname.slice(1);
960
+ parsed.pathname = "/postgres";
961
+ const client = new Client({ connectionString: parsed.toString() });
962
+ await client.connect();
963
+ try {
964
+ await client.query(`DROP DATABASE IF EXISTS "${devDbName}"`);
965
+ out(chalk.gray(` ✓ Removed validation database "${devDbName}"`));
966
+ } finally {
967
+ await client.end();
968
+ }
969
+ } catch (err) {
970
+ outWarn(chalk.gray(` ⚠ Could not remove the Atlas scratch database: ${err instanceof Error ? err.message : String(err)}\n \`rebase db branch prune --include-dev-diff\` clears them.`));
971
+ }
972
+ }
973
+ /**
974
+ * The generated SQL for the project's `search` blocks, if it has any.
975
+ *
976
+ * @param sqlDir directory holding the generated SQL. Defaults to
977
+ * `.rebase/sql` under the working directory.
978
+ */
979
+ function readSearchDdl(sqlDir = generatedSqlDir()) {
980
+ const searchFile = path.join(sqlDir, "search.sql");
981
+ if (!fs.existsSync(searchFile)) return "";
982
+ return fs.readFileSync(searchFile, "utf-8").trim();
983
+ }
984
+ /**
985
+ * Bring the search column, its index and their helpers up to date.
986
+ *
987
+ * Runs *after* Atlas, not before: the statements are `ALTER TABLE ... ADD
988
+ * COLUMN`, so the table has to exist. Atlas is told to ignore these objects
989
+ * entirely (`getSearchExcludes`) — it cannot manage them, and left to itself it
990
+ * would plan a `DROP COLUMN` for every one, since they are absent from the
991
+ * desired state it was given.
992
+ *
993
+ * A no-op when no collection declared `search`. A failure is *not* swallowed:
994
+ * silently pushing a schema whose search column never appeared is how a
995
+ * collection ends up with search configured, no error anywhere, and no results.
996
+ */
997
+ async function applySearchDdl(databaseUrl, sqlDir = generatedSqlDir()) {
998
+ const sql = readSearchDdl(sqlDir);
999
+ if (!sql) return;
1000
+ await withAppClient(databaseUrl, (client) => client.query(sql));
1001
+ out(chalk.gray(" ✓ Applied full-text search columns and indexes"));
1002
+ }
1003
+ /**
1004
+ * Glob patterns keeping Atlas away from the search objects.
1005
+ *
1006
+ * Returns an empty list — and so changes nothing — for a project with no
1007
+ * `search` block, which is every project that has not opted in.
1008
+ */
1009
+ async function getSearchExcludes(collectionsPath) {
1010
+ const { searchExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1011
+ return searchExcludePatterns(await loadCollectionsForCli(collectionsPath));
1012
+ }
1013
+ /**
1014
+ * Every `generated column → column it reads` edge in the database.
1015
+ *
1016
+ * Read from `pg_depend` rather than by matching names in the expression text:
1017
+ * a generated column's `pg_attrdef` row carries one `deptype = 'n'` dependency
1018
+ * per column its expression references, which is the catalogue's own answer and
1019
+ * cannot be fooled by a column name that also appears inside a string literal
1020
+ * or as part of a longer identifier.
1021
+ *
1022
+ * Used by `db push` to find what has to come out of the way before Atlas runs —
1023
+ * see `generated-column-conflicts.ts` for why that is necessary at all.
1024
+ */
1025
+ async function queryGeneratedColumnDependencies(databaseUrl) {
1026
+ return withAppClient(databaseUrl, async (client) => {
1027
+ return (await client.query(`
1028
+ SELECT n.nspname AS "schema",
1029
+ c.relname AS "table",
1030
+ gen.attname AS "column",
1031
+ dep.attname AS "dependsOn"
1032
+ FROM pg_attrdef ad
1033
+ JOIN pg_class c ON c.oid = ad.adrelid
1034
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1035
+ JOIN pg_attribute gen ON gen.attrelid = ad.adrelid AND gen.attnum = ad.adnum
1036
+ JOIN pg_depend d ON d.classid = 'pg_attrdef'::regclass
1037
+ AND d.objid = ad.oid
1038
+ AND d.refclassid = 'pg_class'::regclass
1039
+ AND d.refobjid = ad.adrelid
1040
+ AND d.deptype = 'n'
1041
+ JOIN pg_attribute dep ON dep.attrelid = ad.adrelid AND dep.attnum = d.refobjsubid
1042
+ WHERE gen.attgenerated <> ''
1043
+ AND NOT gen.attisdropped
1044
+ AND NOT dep.attisdropped;
1045
+ `)).rows;
1046
+ });
1047
+ }
1048
+ /**
1049
+ * Every column in the database with its current type, for the destructive
1050
+ * gate to tell a widening `ALTER COLUMN … TYPE` from one that loses data.
1051
+ */
1052
+ async function queryColumnTypes(databaseUrl) {
1053
+ return withAppClient(databaseUrl, async (client) => (await client.query(COLUMN_TYPES_SQL)).rows);
1054
+ }
1055
+ /**
1056
+ * Drop the generated columns standing in the way of an apply.
1057
+ *
1058
+ * Their indexes and their `COMMENT ON COLUMN` stamp go with them, which is
1059
+ * exactly right: `applySearchDdl` rebuilds all three from `search.sql` and a
1060
+ * surviving stamp would make the rebuilt column look like drift.
1061
+ */
1062
+ async function dropGeneratedColumns(databaseUrl, statements) {
1063
+ if (statements.length === 0) return;
1064
+ await withAppClient(databaseUrl, async (client) => {
1065
+ for (const statement of statements) await client.query(statement);
1066
+ });
1067
+ }
1068
+ /**
1069
+ * The generated SQL for the project's `vector` properties, if it has any.
1070
+ *
1071
+ * @param sqlDir directory holding the generated SQL. Defaults to
1072
+ * `.rebase/sql` under the working directory.
1073
+ */
1074
+ function readVectorDdl(sqlDir = generatedSqlDir()) {
1075
+ const vectorFile = path.join(sqlDir, "vector.sql");
1076
+ if (!fs.existsSync(vectorFile)) return "";
1077
+ return fs.readFileSync(vectorFile, "utf-8").trim();
1078
+ }
1079
+ /**
1080
+ * Install pgvector and bring the vector columns and their ANN indexes up to
1081
+ * date.
1082
+ *
1083
+ * Runs *after* Atlas, like {@link applySearchDdl} and for the same reason: the
1084
+ * statements are `ALTER TABLE … ADD COLUMN`, so the table has to exist. Atlas
1085
+ * is told to ignore these objects entirely ({@link getVectorExcludes}) — it
1086
+ * cannot manage a type its dev database is structurally incapable of having,
1087
+ * and left to itself it would plan a `DROP COLUMN` for every one.
1088
+ *
1089
+ * A no-op when no collection declares a `vector` property. A failure is *not*
1090
+ * swallowed: a push that reported success while the embedding column never
1091
+ * appeared is how a project ends up with vector search configured, no error
1092
+ * anywhere, and no results.
1093
+ */
1094
+ async function applyVectorDdl(databaseUrl, sqlDir = generatedSqlDir()) {
1095
+ const sql = readVectorDdl(sqlDir);
1096
+ if (!sql) return;
1097
+ const { vectorExtensionHint } = await import("./vector-index-CMJKb3C9.js").then((n) => n.f);
1098
+ try {
1099
+ await withAppClient(databaseUrl, (client) => client.query(sql));
1100
+ } catch (err) {
1101
+ const message = err instanceof Error ? err.message : String(err);
1102
+ const hint = vectorExtensionHint(message);
1103
+ if (!hint) throw err;
1104
+ throw new Error(`${message}${hint}`, { cause: err });
1105
+ }
1106
+ out(chalk.gray(" ✓ Applied vector columns and ANN indexes"));
1107
+ }
1108
+ /**
1109
+ * Glob patterns keeping Atlas away from the vector objects.
1110
+ *
1111
+ * Returns an empty list — and so changes nothing — for a project with no
1112
+ * `vector` property, which is every project that has not opted in.
1113
+ */
1114
+ async function getVectorExcludes(collectionsPath) {
1115
+ const { vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1116
+ return vectorExcludePatterns(await loadCollectionsForCli(collectionsPath));
1117
+ }
1118
+ /**
1119
+ * The generated SQL for the project's `autoValue: "on_update"` properties, if
1120
+ * it has any.
1121
+ *
1122
+ * @param sqlDir directory holding the generated SQL. Defaults to
1123
+ * `.rebase/sql` under the working directory.
1124
+ */
1125
+ function readTriggersDdl(sqlDir = generatedSqlDir()) {
1126
+ const triggersFile = path.join(sqlDir, "triggers.sql");
1127
+ if (!fs.existsSync(triggersFile)) return "";
1128
+ return fs.readFileSync(triggersFile, "utf-8").trim();
1129
+ }
1130
+ /**
1131
+ * Install the `updated_at` trigger function and bind it to every column that
1132
+ * asked for one.
1133
+ *
1134
+ * Runs *after* Atlas, like {@link applySearchDdl} and {@link applyVectorDdl}:
1135
+ * a trigger needs its table and its column to exist. Atlas is told to ignore
1136
+ * the triggers ({@link getTriggerExcludes}) because it cannot manage a function
1137
+ * at all — its free tier refuses to parse a desired state containing one.
1138
+ *
1139
+ * A no-op when no property declares `autoValue: "on_update"`. A failure is
1140
+ * *not* swallowed: a push that reported success while the trigger never
1141
+ * appeared is how `updated_at` ends up silently stale for every writer that is
1142
+ * not the driver, which is the whole reason the trigger exists.
1143
+ */
1144
+ async function applyTriggersDdl(databaseUrl, sqlDir = generatedSqlDir()) {
1145
+ const sql = readTriggersDdl(sqlDir);
1146
+ if (!sql) return;
1147
+ await withAppClient(databaseUrl, (client) => client.query(sql));
1148
+ out(chalk.gray(" ✓ Applied `updated_at` triggers"));
1149
+ }
1150
+ /** @see getSearchExcludes */
1151
+ async function getTriggerExcludes(collectionsPath) {
1152
+ const { triggerExcludePatterns } = await import("./generate-postgres-ddl-logic-B64O2oZF.js").then((n) => n.o);
1153
+ return triggerExcludePatterns(await loadCollectionsForCli(collectionsPath));
1154
+ }
1155
+ /**
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`.
1163
+ *
1164
+ * Constraint-backed indexes are left out: a `PRIMARY KEY` or a `UNIQUE` is a
1165
+ * *constraint* to Atlas, diffed from the constraint declaration, and naming its
1166
+ * index in an exclude would shield the constraint itself.
1167
+ */
1168
+ async function queryExistingIndexes(databaseUrl) {
1169
+ return withAppClient(databaseUrl, async (client) => {
1170
+ return (await client.query(`
1171
+ SELECT n.nspname || '.' || t.relname || '.' || i.relname AS full_name
1172
+ FROM pg_index x
1173
+ JOIN pg_class i ON i.oid = x.indexrelid
1174
+ JOIN pg_class t ON t.oid = x.indrelid
1175
+ JOIN pg_namespace n ON n.oid = t.relnamespace
1176
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
1177
+ AND t.relkind IN ('r', 'p')
1178
+ AND NOT EXISTS (
1179
+ SELECT 1 FROM pg_constraint c WHERE c.conindid = i.oid
1180
+ );
1181
+ `)).rows.map((row) => row.full_name);
1182
+ });
1183
+ }
1184
+ /**
1185
+ * Glob patterns keeping Atlas away from indexes that are not Rebase's.
1186
+ *
1187
+ * The problem this solves is live and predates the `indexes:` block. `db push`
1188
+ * is declarative, so an index on a managed table that is absent from
1189
+ * `schema.sql` is drift — and Atlas plans `DROP INDEX` for it. Verified
1190
+ * against atlas v1.2.3: create one by hand, re-run an unchanged push, and the
1191
+ * plan is a bare drop. `DROP INDEX` is not in `DESTRUCTIVE_PATTERNS`, so the
1192
+ * auto-approved apply took it without asking. Until now the only way to have
1193
+ * an index at all was to write it by hand, which made that the *only* outcome.
1194
+ *
1195
+ * Ownership rather than a prompt, because once indexes are declarable a drop
1196
+ * is usually correct: removing one from the config should remove it from the
1197
+ * database, quietly. What must never be dropped is an index Rebase never
1198
+ * created — a hand-written one, or one an introspected database arrived with.
1199
+ * {@link isRebaseIndexName} is the test, the same arrangement
1200
+ * `isGeneratedPolicyName` uses to let policy reconciliation drop only what it
1201
+ * generated.
1202
+ *
1203
+ * The named-in-the-desired-state check comes first and is what makes removal
1204
+ * work: a declared index that the author has just deleted still matches the
1205
+ * name pattern, is no longer in the plan, and so is *not* excluded — Atlas
1206
+ * drops it, as intended.
1207
+ *
1208
+ * Three-part `schema.table.index`, never two: a two-part pattern reads as a
1209
+ * *table* named `<index>` in a *schema* named `<table>`, matches nothing, and
1210
+ * reports no error — the trap already recorded for the search excludes.
1211
+ */
1212
+ async function getForeignIndexExcludes(databaseUrl, collectionsPath, deps = {}) {
1213
+ const queryIndexes = deps.queryExistingIndexes ?? queryExistingIndexes;
1214
+ const managed = await (deps.getManagedIndexNames ?? managedIndexNames)(collectionsPath);
1215
+ let existing;
1216
+ try {
1217
+ existing = await queryIndexes(databaseUrl);
1218
+ } catch (err) {
1219
+ throw new ExcludeIntrospectionError(`Failed to introspect the database for unmanaged indexes: ${err instanceof Error ? err.message : String(err)}`, err);
1220
+ }
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
+ });
1226
+ }
1227
+ /**
1228
+ * The index names the current collections would create — search, vector and
1229
+ * declared alike. Anything Atlas would emit is by definition not foreign.
1230
+ */
1231
+ async function managedIndexNames(collectionsPath) {
1232
+ 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);
1236
+ 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));
1238
+ return names;
1239
+ }
1240
+ async function queryExistingTables(databaseUrl) {
1241
+ return withAppClient(databaseUrl, async (client) => {
1242
+ return (await client.query(`
1243
+ SELECT table_schema || '.' || table_name AS full_name
1244
+ FROM information_schema.tables
1245
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
1246
+ AND table_type IN ('BASE TABLE', 'VIEW');
1247
+ `)).rows.map((row) => row.full_name);
1248
+ });
1249
+ }
1250
+ /**
1251
+ * Raised when the exclude list could not be built. `db push` MUST abort on
1252
+ * this rather than continue: the exclude list is the only thing shielding
1253
+ * non-collection (user/system) tables from the auto-approved declarative
1254
+ * apply. A partial list — the old fail-open behaviour — meant a transient
1255
+ * introspection hiccup dropped every table not present in `schema.sql`.
1256
+ */
1257
+ var ExcludeIntrospectionError = class extends Error {
1258
+ cause;
1259
+ constructor(message, cause) {
1260
+ super(message);
1261
+ this.cause = cause;
1262
+ this.name = "ExcludeIntrospectionError";
1263
+ }
1264
+ };
1265
+ /**
1266
+ * Build the `--exclude` list that protects tables Rebase doesn't manage from
1267
+ * the declarative apply. Anything not backing a collection (or its M2M
1268
+ * junctions) is excluded so Atlas never drops it.
1269
+ *
1270
+ * Fails **closed**: if the database can't be introspected we cannot know
1271
+ * which tables to protect, so we throw {@link ExcludeIntrospectionError}
1272
+ * instead of returning a near-empty list and letting the caller drop
1273
+ * everything.
1274
+ *
1275
+ * `deps` is injectable for tests; production uses the real pg-backed queries.
1276
+ */
1277
+ async function getTableExcludes(databaseUrl, collectionsPath, deps = {}) {
1278
+ const getIncludes = deps.getIncludes ?? getTableIncludes;
1279
+ const queryTables = deps.queryExistingTables ?? queryExistingTables;
1280
+ const includes = await getIncludes(collectionsPath);
1281
+ const excludes = [
1282
+ "atlas_schema_revisions.*",
1283
+ "auth",
1284
+ "auth.*",
1285
+ "rebase",
1286
+ "rebase.*"
1287
+ ];
1288
+ let existingTables;
1289
+ try {
1290
+ existingTables = await queryTables(databaseUrl);
1291
+ } catch (err) {
1292
+ throw new ExcludeIntrospectionError(`Failed to introspect the database for unmapped tables: ${err instanceof Error ? err.message : String(err)}`, err);
1293
+ }
1294
+ for (const table of existingTables) if (!includes.includes(table)) excludes.push(table);
1295
+ return excludes;
1296
+ }
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
+ //#endregion
1325
+ //#region src/cli-errors.ts
1326
+ /**
1327
+ * Detect whether an error (or AggregateError wrapping multiple attempts)
1328
+ * represents an ECONNREFUSED — i.e. the database is simply not running.
1329
+ *
1330
+ * Handles:
1331
+ * - Direct `{ code: "ECONNREFUSED" }` errors from Node `net`
1332
+ * - `AggregateError` from dual-stack IPv4+IPv6 connection attempts
1333
+ * - Drizzle's `cause`-wrapped pg errors
1334
+ */
1335
+ function isEconnrefused(err) {
1336
+ if (!err || typeof err !== "object") return false;
1337
+ const e = err;
1338
+ if (e.code === "ECONNREFUSED") return true;
1339
+ if (Array.isArray(e.errors)) return e.errors.some((inner) => inner && typeof inner === "object" && inner.code === "ECONNREFUSED");
1340
+ if (e.cause && typeof e.cause === "object") return isEconnrefused(e.cause);
1341
+ return false;
1342
+ }
1343
+ /**
1344
+ * Detect PostgreSQL authentication failures.
1345
+ * PG error codes: 28P01 (invalid_password), 28000 (invalid_authorization_specification)
1346
+ */
1347
+ function isAuthFailure(err) {
1348
+ if (!err || typeof err !== "object") return false;
1349
+ const e = err;
1350
+ if (e.code === "28P01" || e.code === "28000") return true;
1351
+ if (e.cause && typeof e.cause === "object") return isAuthFailure(e.cause);
1352
+ if ("message" in e && typeof e.message === "string") {
1353
+ const msg = e.message.toLowerCase();
1354
+ if (msg.includes("password authentication failed") || msg.includes("no pg_hba.conf entry")) return true;
1355
+ }
1356
+ return false;
1357
+ }
1358
+ /**
1359
+ * Detect the "SSL is not enabled on the server" failure — the client attempted
1360
+ * an SSL handshake against a Postgres server that doesn't support it (common
1361
+ * with a plain local dev database). The fix is `?sslmode=disable` on the URL.
1362
+ */
1363
+ function isSslNotEnabled(err) {
1364
+ if (!err || typeof err !== "object") return false;
1365
+ const e = err;
1366
+ if (typeof e.message === "string" && e.message.toLowerCase().includes("ssl is not enabled on the server")) return true;
1367
+ if (e.cause && typeof e.cause === "object") return isSslNotEnabled(e.cause);
1368
+ return false;
1369
+ }
1370
+ /**
1371
+ * Detect PostgreSQL "cannot drop ... because other objects depend on it"
1372
+ * (error code 2BP01, dependent_objects_still_exist). This is the failure that
1373
+ * strands a declarative `db push` half-applied when a collection is removed but
1374
+ * an enum type it defined is still referenced by another object.
1375
+ */
1376
+ function isDependencyDropError(err) {
1377
+ if (!err || typeof err !== "object") return false;
1378
+ const e = err;
1379
+ if (e.code === "2BP01") return true;
1380
+ if (typeof e.message === "string") {
1381
+ const msg = e.message.toLowerCase();
1382
+ if (msg.includes("other objects depend on it") || msg.includes("cannot drop type")) return true;
1383
+ }
1384
+ if (e.cause && typeof e.cause === "object") return isDependencyDropError(e.cause);
1385
+ return false;
1386
+ }
1387
+ /**
1388
+ * Parse host:port from a DATABASE_URL for display purposes.
1389
+ *
1390
+ * Exported because every message about a connection has to name the thing it
1391
+ * could not reach, and the boot path needs the same rendering the CLI banners
1392
+ * use — including the same refusal to print the URL itself, which carries the
1393
+ * password.
1394
+ */
1395
+ function parseHostInfo(databaseUrl) {
1396
+ try {
1397
+ const parsed = new URL(databaseUrl);
1398
+ return `${parsed.hostname}:${parsed.port || 5432}`;
1399
+ } catch {
1400
+ return "unknown";
1401
+ }
1402
+ }
1403
+ /**
1404
+ * The sentence the operating system actually produced, dug out of the wrappers.
1405
+ *
1406
+ * `connect ECONNREFUSED 127.0.0.1:5432` is written by `net`, then wrapped by
1407
+ * `pg`, then wrapped again by Drizzle as `Failed query: …` — and on a
1408
+ * dual-stack host it is not in `.cause` at all but inside the
1409
+ * `AggregateError.errors` array of one attempt per resolved address. Printing
1410
+ * the banner without it loses the one token every search engine, runbook and
1411
+ * colleague recognises.
1412
+ */
1413
+ function deepestErrorMessage(err) {
1414
+ if (!err || typeof err !== "object") return null;
1415
+ const e = err;
1416
+ if (Array.isArray(e.errors)) for (const inner of e.errors) {
1417
+ const deeper = deepestErrorMessage(inner);
1418
+ if (deeper) return deeper;
1419
+ }
1420
+ if (e.cause) {
1421
+ const deeper = deepestErrorMessage(e.cause);
1422
+ if (deeper) return deeper;
1423
+ }
1424
+ if (typeof e.message === "string" && e.message && !e.message.startsWith("Failed query:")) return e.code ? `${e.message} (${e.code})` : e.message;
1425
+ return null;
1426
+ }
1427
+ /** One indented line naming the driver's own reason, or nothing. */
1428
+ function driverReasonLine(err) {
1429
+ const reason = err === void 0 ? null : deepestErrorMessage(err);
1430
+ return reason ? ` The driver said: ${reason}\n\n` : "";
1431
+ }
1432
+ /**
1433
+ * Format a diagnostic banner for ECONNREFUSED errors.
1434
+ */
1435
+ function formatConnectionRefusedBanner(databaseUrl, err) {
1436
+ return `
1437
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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";
1439
+ }
1440
+ /**
1441
+ * Format a diagnostic banner for authentication failures.
1442
+ */
1443
+ function formatAuthFailureBanner(databaseUrl, err) {
1444
+ const hostInfo = parseHostInfo(databaseUrl);
1445
+ let username = "unknown";
1446
+ try {
1447
+ username = new URL(databaseUrl).username || "unknown";
1448
+ } catch {}
1449
+ return `
1450
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1451
+ ❌ Authentication failed for user "${username}" at ${hostInfo}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + driverReasonLine(err) + ` PostgreSQL rejected the credentials. Common fixes:
1452
+
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`;
1456
+ }
1457
+ /**
1458
+ * Format a diagnostic banner for "SSL is not enabled on the server".
1459
+ */
1460
+ function formatSslNotEnabledBanner(databaseUrl) {
1461
+ const hostInfo = parseHostInfo(databaseUrl);
1462
+ const suggestion = databaseUrl.includes("?") ? "&sslmode=disable" : "?sslmode=disable";
1463
+ return `
1464
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1465
+ ❌ SSL is not enabled on the PostgreSQL server at ${hostInfo}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n The client tried to connect over SSL, but the server does not\n support it. This is normal for a plain local dev database.\n\n Fix: append ${chalk.bold("sslmode=disable")} to DATABASE_URL, e.g.\n\n DATABASE_URL=...${suggestion}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1466
+ }
1467
+ /**
1468
+ * Format a diagnostic banner for a dependency-drop failure during `db push`.
1469
+ * Explains that the database may be left partially migrated and how to recover.
1470
+ */
1471
+ function formatDependencyDropBanner() {
1472
+ return `
1473
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1474
+ ❌ Schema push failed: a type/table could not be dropped
1475
+ because other objects still depend on it.
1476
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1477
+
1478
+ ${chalk.yellow("The database may now be partially migrated.")} Atlas applies\n statements individually, so earlier changes in this push may\n already be committed while later ones failed.\n\n This commonly happens when a collection is removed but an enum\n type it defined is still referenced. To recover:\n\n 1. Inspect the leftover object named in the error above.\n 2. Drop it with CASCADE, e.g.:\n psql "$DATABASE_URL" -c 'DROP TYPE "<name>" CASCADE;'\n 3. Re-run: ${chalk.bold.green("rebase db push")}\n\n Prefer a safe, versioned workflow? Use ${chalk.bold("rebase db generate")}\n + ${chalk.bold("rebase db migrate")} instead of push for destructive changes.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1479
+ }
1480
+ /**
1481
+ * Pre-flight check: verify that the database is reachable before running
1482
+ * a heavy subprocess (Atlas, migrations, etc.).
1483
+ *
1484
+ * Exits with code 1 and a friendly banner on known failure modes.
1485
+ * On unknown errors, logs a warning and allows the caller to proceed.
1486
+ */
1487
+ async function checkDatabaseConnectivity(databaseUrl) {
1488
+ try {
1489
+ await withAppClient(databaseUrl, (client) => client.query("SELECT 1"), { connectionTimeoutMillis: 5e3 });
1490
+ } catch (err) {
1491
+ if (isEconnrefused(err)) {
1492
+ outError(formatConnectionRefusedBanner(databaseUrl, err));
1493
+ process.exit(1);
1494
+ }
1495
+ if (isAuthFailure(err)) {
1496
+ outError(formatAuthFailureBanner(databaseUrl, err));
1497
+ process.exit(1);
1498
+ }
1499
+ if (isSslNotEnabled(err)) {
1500
+ outError(formatSslNotEnabledBanner(databaseUrl));
1501
+ process.exit(1);
1502
+ }
1503
+ outWarn(chalk.yellow(` ⚠ Could not verify database connectivity: ${err instanceof Error ? err.message : String(err)}`));
1504
+ outWarn(chalk.gray(" Proceeding anyway — the command may fail if the database is unreachable."));
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Post-hoc error diagnosis for direct database operations (e.g. applyPolicies).
1509
+ * Returns a formatted diagnostic string if the error matches a known pattern,
1510
+ * or null if unrecognized.
1511
+ */
1512
+ function diagnoseDbError(err, databaseUrl) {
1513
+ if (isEconnrefused(err)) return formatConnectionRefusedBanner(databaseUrl || "", err);
1514
+ if (isAuthFailure(err)) return formatAuthFailureBanner(databaseUrl || "", err);
1515
+ if (isSslNotEnabled(err)) return formatSslNotEnabledBanner(databaseUrl || "");
1516
+ if (isDependencyDropError(err)) return formatDependencyDropBanner();
1517
+ return null;
1518
+ }
1519
+ /**
1520
+ * Say why the command failed, on the way out.
1521
+ *
1522
+ * The entry point below used to be `.catch(() => process.exit(1))`, which threw
1523
+ * the error away. Every message this file and its services raise — "Branch
1524
+ * \"x\" already exists.", "the source database has active connections", "Branch
1525
+ * name is too long" — was written, wrapped in the right PG error code, and then
1526
+ * discarded one frame before it reached a terminal. What a developer saw was a
1527
+ * header line, no error, and exit 1.
1528
+ *
1529
+ * Two shapes are deliberately kept quiet:
1530
+ *
1531
+ * - **A child process that already spoke.** Atlas, `pg_dump` and `psql` run
1532
+ * with inherited stdio, so their diagnosis is on the terminal already and
1533
+ * execa's wrapper adds only `Command failed with exit code 1: atlas …`.
1534
+ * `packages/cli` filters exactly these two phrasings one level up
1535
+ * (`runDbCommand`), and this is that filter, for the process that is actually
1536
+ * throwing.
1537
+ *
1538
+ * - **A message that is only a query.** Drizzle reports failures as
1539
+ * `Failed query: <sql> params:` and hides the real PostgreSQL error in
1540
+ * `cause`, so the wrapper alone tells a reader nothing they can act on. The
1541
+ * cause is appended when it says something the message does not.
1542
+ *
1543
+ * - **An error that has already printed its own diagnosis**, marked
1544
+ * `alreadyReported`. `CollectionsPathMissing` is one: it prints the path, what
1545
+ * it resolved to and the cwd it resolved against, and then throws so the entry
1546
+ * point owns the exit code. Repeating its one-line summary underneath would
1547
+ * undo the "printed once" this whole path exists for.
1548
+ */
1549
+ function reportCommandFailure(error) {
1550
+ const message = error instanceof Error ? error.message : String(error ?? "");
1551
+ if (error?.alreadyReported) return;
1552
+ if (!message || /Command failed|exited with code/i.test(message)) return;
1553
+ outError("");
1554
+ outError(chalk.red(` ✗ ${message}`));
1555
+ const cause = extractCauseMessage(error);
1556
+ if (cause && !message.includes(cause)) outError(chalk.gray(` ${cause}`));
1557
+ outError("");
1558
+ }
1559
+ /**
1560
+ * `migrate apply` refusing because the database already has the schema.
1561
+ *
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
1565
+ * database, this is the normal case rather than the exotic one, and the raw
1566
+ * failure (`pq: type "posts_status" already exists (42710)`, then `sql/migrate:
1567
+ * write revision: … current transaction is aborted`) names no way forward.
1568
+ */
1569
+ function parseAlreadyProvisioned(text) {
1570
+ const withCode = /(?:type|relation|constraint|schema) "([^"]+)" already exists \((42710|42P07)\)/.exec(text);
1571
+ if (withCode) return {
1572
+ object: withCode[1],
1573
+ code: withCode[2]
1574
+ };
1575
+ const bare = /(?:type|relation|constraint|schema) "([^"]+)" already exists/.exec(text);
1576
+ if (bare && /42710|42P07/.test(text)) return {
1577
+ object: bare[1],
1578
+ code: /42P07/.test(text) ? "42P07" : "42710"
1579
+ };
1580
+ return null;
1581
+ }
1582
+ /**
1583
+ * What to do about it: tell Atlas the database is already at a version.
1584
+ *
1585
+ * Atlas's own mechanism (`migrate apply --baseline <version>`), not a ledger of
1586
+ * ours — it writes the revision row Atlas reads, so every later `rebase db
1587
+ * migrate` is an ordinary one.
1588
+ */
1589
+ function formatBaselineRemedy(version, object) {
1590
+ 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`;
1592
+ }
1593
+ /** `ALTER TABLE … SET NOT NULL` on a table that already holds rows (23502). */
1594
+ function parseNotNullViolation(text) {
1595
+ const match = /column "([^"]+)" of relation "([^"]+)" contains null values/.exec(text);
1596
+ return match ? {
1597
+ table: match[2],
1598
+ column: match[1]
1599
+ } : null;
1600
+ }
1601
+ /**
1602
+ * Whether the column that refused NOT NULL was being added by this push, read
1603
+ * from the statement Atlas was running: `true` for `ADD COLUMN "<column>"`,
1604
+ * `false` for any other statement that names it (`ALTER COLUMN … SET NOT
1605
+ * NULL`), `null` when the error carried no statement.
1606
+ */
1607
+ function notNullColumnIsNew(stderr, column) {
1608
+ const statement = parseFailedStatement(stderr);
1609
+ if (!statement) return null;
1610
+ const quoted = column.replace(/"/g, "\"\"").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1611
+ return new RegExp(`ADD\\s+COLUMN\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?"${quoted}"`, "i").test(statement);
1612
+ }
1613
+ /**
1614
+ * The ways out, and only the ones that work for this column.
1615
+ *
1616
+ * A `defaultValue` backfills only a column the push is adding: PostgreSQL
1617
+ * fills `ADD COLUMN … NOT NULL DEFAULT …` for every existing row, but `ALTER
1618
+ * COLUMN … SET DEFAULT` touches no row that is already there, so for an
1619
+ * existing column that remedy fails exactly as before. When the error does not
1620
+ * say which it was (`isNew` null), the default is offered with that caveat.
1621
+ *
1622
+ * Boot-ensure handles this case — it adds the column nullable and sets NOT NULL
1623
+ * only when the table is empty — so a push that dies here is strictly worse
1624
+ * than the boot that would have run instead.
1625
+ */
1626
+ function formatNotNullViolationBanner(violation, rowCount, isNew = null) {
1627
+ const { table, column } = violation;
1628
+ const rows = rowCount === null ? ` "${table}" already holds rows, and none of them has a value for "${column}".\n` : ` "${table}" already holds ${rowCount} row${rowCount === 1 ? "" : "s"}, and they have no value for "${column}".\n`;
1629
+ const backfill = `Backfill by hand first, then push again:\n ${chalk.gray(`psql "$DATABASE_URL" -c 'UPDATE "${table}" SET "${column}" = … WHERE "${column}" IS NULL;'`)}\n`;
1630
+ const leaveOptional = `Leave it optional — take \`validation.required\` off the property.\n`;
1631
+ const defaultLine = (caveat) => `Give the property a default, so the push can backfill${caveat}:\n ${chalk.gray(`{ type: "string", defaultValue: "…", validation: { required: true } }`)}\n`;
1632
+ const remedies = isNew === false ? [backfill, leaveOptional] : isNew === true ? [
1633
+ defaultLine(""),
1634
+ backfill,
1635
+ leaveOptional
1636
+ ] : [
1637
+ backfill,
1638
+ defaultLine(" (only if this push adds the column — a default never fills rows that already exist)"),
1639
+ leaveOptional
1640
+ ];
1641
+ const count = [
1642
+ "",
1643
+ "One way",
1644
+ "Two ways",
1645
+ "Three ways"
1646
+ ][remedies.length];
1647
+ return `
1648
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1649
+ ❌ Cannot make "${table}"."${column}" required: existing rows are null\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + rows + ` PostgreSQL will not add the NOT NULL until every one of them does.
1650
+
1651
+ ${count} forward:\n\n` + remedies.map((r, i) => ` ${i + 1}. ${r}`).join("") + `\n ${chalk.gray("Nothing in this push was applied: Atlas applies it as one transaction.")}\n ${chalk.gray("`rebase dev` would have kept the column nullable instead:")}\n ${chalk.gray("boot sets NOT NULL only when it is safe.")}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1652
+ }
1653
+ /** Atlas refusing to remove a label from an enum — a renamed `enum` option id. */
1654
+ function parseEnumLabelDrop(text) {
1655
+ const match = /dropping (?:enum )?value "([^"]+)" from enum "([^"]+)" is not supported/.exec(text);
1656
+ return match ? {
1657
+ label: match[1],
1658
+ enumType: match[2]
1659
+ } : null;
1660
+ }
1661
+ /**
1662
+ * Why the two paths disagree, and what retiring an option id actually costs.
1663
+ *
1664
+ * Boot-ensure adds enum labels and never removes one, so `rebase dev` accepts
1665
+ * the very edit that stops `db push` dead. PostgreSQL has no `ALTER TYPE …
1666
+ * DROP VALUE`: a label goes only by rewriting the type, which is a data
1667
+ * migration and not a schema push.
1668
+ */
1669
+ function formatEnumLabelDropBanner(drop) {
1670
+ const { label, enumType } = drop;
1671
+ return `
1672
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1673
+ ❌ "${label}" cannot be removed from the enum "${enumType}"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n PostgreSQL has no ALTER TYPE … DROP VALUE. An option id can be added\n to an enum but never taken out of one, so renaming an id reads as a\n removal and a push cannot carry it.\n\n ${chalk.yellow("Nothing was applied.")} ${chalk.bold("rebase dev")} accepts this same edit — boot adds\n new labels and never removes one — so the two paths disagree by\n design, and the old label would simply have stayed behind, unused.\n\n To keep the id: put "${label}" back and change only its label text.\n\n To retire it for real, it is a data migration:\n\n 1. Add the new id alongside the old one, and push.\n 2. Move the rows: ${chalk.gray(`UPDATE … SET … = '<new>' WHERE … = '${label}';`)}\n 3. Rewrite the type, which is what actually drops the label:\n ${chalk.gray("rebase db generate retire_option && rebase db migrate")}\n ${chalk.gray("(a migration can CREATE TYPE …, ALTER TABLE … TYPE … USING, DROP TYPE)")}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1674
+ }
1675
+ /**
1676
+ * The statement Atlas was running when it failed, unescaped.
1677
+ *
1678
+ * Two spellings, one per phase: the apply quotes it
1679
+ * (`executing statement "ALTER TABLE \"public\".\"people\" …;": pq: …`), and
1680
+ * the dry-run's analysis does not
1681
+ * (`analyzing plan: executing statement: ALTER TABLE "public"."people" …;: pq: …`).
1682
+ */
1683
+ function parseFailedStatement(text) {
1684
+ const quoted = /executing statement "((?:[^"\\]|\\.)*)"/.exec(text);
1685
+ if (quoted) return quoted[1].replace(/\\(.)/g, "$1");
1686
+ const bare = /executing statement: ([\s\S]*?;): pq:/.exec(text);
1687
+ return bare ? bare[1] : null;
1688
+ }
1689
+ /** `ALTER TABLE "public"."people" …` → `{ schema: "public", table: "people" }`. */
1690
+ function failedStatementTable(statement) {
1691
+ const ident = String.raw`(?:"((?:[^"]|"")+)"|([A-Za-z_][A-Za-z0-9_$]*))`;
1692
+ const match = new RegExp(String.raw`^\s*(?:ALTER\s+TABLE|CREATE\s+UNIQUE\s+INDEX\s+\S+\s+ON)\s+(?:ONLY\s+)?${ident}(?:\.${ident})?`, "i").exec(statement);
1693
+ if (!match) return null;
1694
+ const first = (match[1] ?? match[2]).replace(/""/g, "\"");
1695
+ const second = match[3] ?? match[4];
1696
+ return second === void 0 ? { table: first } : {
1697
+ schema: first,
1698
+ table: second.replace(/""/g, "\"")
1699
+ };
1700
+ }
1701
+ /**
1702
+ * PostgreSQL refusing to retype a column a `GENERATED ALWAYS AS … STORED`
1703
+ * expression reads (0A000). `db push` moves Rebase's own search columns out of
1704
+ * the way before the apply; this is what is left when it could not.
1705
+ */
1706
+ function parseGeneratedColumnRefusal(text) {
1707
+ if (!/cannot alter type of a column used by a generated column/.test(text)) return null;
1708
+ return { statement: parseFailedStatement(text) };
1709
+ }
1710
+ /**
1711
+ * Which generated columns read a column the failed statement retypes — read
1712
+ * from the catalogue, because PostgreSQL's DETAIL naming them does not survive
1713
+ * Atlas.
1714
+ */
1715
+ async function generatedColumnsBlocking(databaseUrl, statement) {
1716
+ if (!databaseUrl || !statement) return [];
1717
+ const target = failedStatementTable(statement);
1718
+ if (!target) return [];
1719
+ const retyped = [...statement.matchAll(/ALTER\s+(?:COLUMN\s+)?"((?:[^"]|"")+)"\s+(?:SET\s+DATA\s+)?TYPE\b/gi)].map((match) => match[1].replace(/""/g, "\""));
1720
+ if (retyped.length === 0) return [];
1721
+ try {
1722
+ const { Client } = await import("pg");
1723
+ const client = new Client({
1724
+ connectionString: databaseUrl,
1725
+ connectionTimeoutMillis: 5e3
1726
+ });
1727
+ await client.connect();
1728
+ try {
1729
+ return (await client.query(`
1730
+ SELECT n.nspname AS "schema", c.relname AS "table", gen.attname AS "column", dep.attname AS "reads"
1731
+ FROM pg_attrdef ad
1732
+ JOIN pg_class c ON c.oid = ad.adrelid
1733
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1734
+ JOIN pg_attribute gen ON gen.attrelid = ad.adrelid AND gen.attnum = ad.adnum
1735
+ JOIN pg_depend d ON d.classid = 'pg_attrdef'::regclass AND d.objid = ad.oid
1736
+ AND d.refclassid = 'pg_class'::regclass AND d.refobjid = ad.adrelid
1737
+ JOIN pg_attribute dep ON dep.attrelid = ad.adrelid AND dep.attnum = d.refobjsubid
1738
+ WHERE gen.attgenerated <> '' AND c.relname = $1
1739
+ AND ($2::text IS NULL OR n.nspname = $2) AND dep.attname = ANY($3::text[])
1740
+ ORDER BY 1, 2, 3, 4`, [
1741
+ target.table,
1742
+ target.schema ?? null,
1743
+ retyped
1744
+ ])).rows;
1745
+ } finally {
1746
+ await client.end();
1747
+ }
1748
+ } catch {
1749
+ return [];
1750
+ }
1751
+ }
1752
+ /**
1753
+ * What to do when a generated column stops a retype.
1754
+ *
1755
+ * A collection's own search column should never reach this — `db push` drops
1756
+ * it before the apply and rebuilds it after — so seeing one named here means
1757
+ * that machinery missed it, which is a bug worth a report. Any other generated
1758
+ * column is somebody's, with an expression Rebase holds no copy of.
1759
+ */
1760
+ function formatGeneratedColumnRefusalBanner(refusal, blockers) {
1761
+ const named = blockers.length > 0 ? blockers.map((b) => ` ${chalk.bold(`${b.schema}.${b.table}.${b.column}`)} reads "${b.reads}"`).join("\n") + "\n\n" : refusal.statement ? ` The statement:\n ${chalk.gray(refusal.statement.replace(/\s+/g, " "))}\n\n` : "";
1762
+ const first = blockers[0];
1763
+ const drop = first ? `ALTER TABLE "${first.schema}"."${first.table}" DROP COLUMN "${first.column}";` : "ALTER TABLE … DROP COLUMN <the generated column>;";
1764
+ return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ A generated column reads a column this push retypes\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + named + ` PostgreSQL will not retype a column while a GENERATED ALWAYS AS …
1765
+ STORED expression reads it, and Atlas applies a push as one
1766
+ transaction — ${chalk.yellow("nothing in this push was applied.")}\n\n A collection's ${chalk.bold("search")} column is dropped and rebuilt around the apply\n by the push itself. If that is the column named above, run the push\n again, and please report it: it should not have got this far.\n\n Any other generated column is yours. Drop it, push, and put it back\n with its original expression:\n\n ${chalk.gray(drop)}\n ${chalk.gray("rebase db push")}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1767
+ }
1768
+ /**
1769
+ * PostgreSQL refusing a retype it has no implicit cast for (42804): `string` →
1770
+ * `number`, `time` → `date`, an array back to a string. Atlas reports it from
1771
+ * the dry-run's analysis as well as from the apply, so without this a
1772
+ * `--dry-run` died with the raw error and showed no plan at all.
1773
+ */
1774
+ function parseUncastableRetype(text) {
1775
+ const match = /column "((?:[^"]|"")+)" cannot be cast automatically to type ([^\n(]+?)\s*(?:\(42804\)|$|\n)/m.exec(text);
1776
+ if (!match) return null;
1777
+ return {
1778
+ column: match[1],
1779
+ type: match[2].trim(),
1780
+ statement: parseFailedStatement(text)
1781
+ };
1782
+ }
1783
+ /** The `ALTER … USING` a developer would run, and the other ways past it. Boot's drift report says the same. */
1784
+ function formatUncastableRetypeBanner(refusal) {
1785
+ const { column, type, statement } = refusal;
1786
+ const target = statement ? failedStatementTable(statement) : null;
1787
+ const table = target ? `${target.schema ? `"${target.schema}".` : ""}"${target.table}"` : "<table>";
1788
+ const quotedColumn = `"${column.replace(/"/g, "\"\"")}"`;
1789
+ return `
1790
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1791
+ ❌ ${table}.${quotedColumn} cannot be converted to ${type} automatically\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n The collection now declares this column as ${type}, and PostgreSQL has\n no implicit conversion from what the column holds today. Atlas stops\n at the first such statement — ${chalk.yellow("nothing in this push was applied")}, and a\n --dry-run cannot show the rest of the plan until this one is settled.\n\n Three ways forward:\n\n 1. Convert the values yourself, saying how, then push again:\n ${chalk.gray(`ALTER TABLE ${table} ALTER COLUMN ${quotedColumn} TYPE ${type} USING ${quotedColumn}::${type};`)}\n ${chalk.gray("(adjust the USING expression to whatever turns each old value into a new one)")}\n 2. Add a new property for the new type, copy the values over with an\n UPDATE, then remove the old property.\n 3. Keep the column as it is — put the property's type back, or pin the\n database type with ${chalk.bold("columnType")}.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1792
+ }
1793
+ /**
1794
+ * A unique constraint or index PostgreSQL could not build because the rows
1795
+ * already repeat a value (23505). The DETAIL naming the value does not survive
1796
+ * Atlas, so the duplicates are read back from the table.
1797
+ */
1798
+ function parseUniqueBuildFailure(text) {
1799
+ const match = /could not create unique index "((?:[^"]|"")+)"/.exec(text);
1800
+ if (!match) return null;
1801
+ const statement = parseFailedStatement(text);
1802
+ let columns = [];
1803
+ if (statement) {
1804
+ const list = /UNIQUE\s*\(([^)]*)\)/i.exec(statement) ?? /\bON\s+(?:ONLY\s+)?(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*)(?:\.(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*))?\s*(?:USING\s+\w+\s*)?\(([^)]*)\)/i.exec(statement);
1805
+ if (list) columns = [...list[1].matchAll(/"((?:[^"]|"")+)"|([A-Za-z_][\w$]*)/g)].map((m) => (m[1] ?? m[2]).replace(/""/g, "\""));
1806
+ }
1807
+ return {
1808
+ index: match[1],
1809
+ statement,
1810
+ columns
1811
+ };
1812
+ }
1813
+ /** Up to five repeated values, read from the database, or `[]` when we cannot say. */
1814
+ async function duplicateValues(databaseUrl, target, columns) {
1815
+ if (!databaseUrl || !target || columns.length === 0) return [];
1816
+ const ident = (name) => `"${name.replace(/"/g, "\"\"")}"`;
1817
+ const table = `${target.schema ? `${ident(target.schema)}.` : ""}${ident(target.table)}`;
1818
+ const cols = columns.map(ident).join(", ");
1819
+ try {
1820
+ const { Client } = await import("pg");
1821
+ const client = new Client({
1822
+ connectionString: databaseUrl,
1823
+ connectionTimeoutMillis: 5e3
1824
+ });
1825
+ await client.connect();
1826
+ try {
1827
+ return (await client.query(`SELECT concat_ws(', ', ${columns.map((c) => `${ident(c)}::text`).join(", ")}) AS "values", count(*)::int AS "count"
1828
+ FROM ${table} GROUP BY ${cols} HAVING count(*) > 1 ORDER BY count(*) DESC LIMIT 5`)).rows;
1829
+ } finally {
1830
+ await client.end();
1831
+ }
1832
+ } catch {
1833
+ return [];
1834
+ }
1835
+ }
1836
+ function formatUniqueBuildFailureBanner(failure, duplicates) {
1837
+ const target = failure.statement ? failedStatementTable(failure.statement) : null;
1838
+ const table = target ? `${target.schema ? `"${target.schema}".` : ""}"${target.table}"` : "<table>";
1839
+ const cols = failure.columns.length > 0 ? failure.columns.map((c) => `"${c}"`).join(", ") : "<column>";
1840
+ const listed = duplicates.length > 0 ? ` Repeated values:\n` + duplicates.map((d) => ` ${chalk.bold(d.values)} ${chalk.gray(`× ${d.count}`)}`).join("\n") + `\n\n` : "";
1841
+ return `
1842
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1843
+ ❌ Cannot make ${table} (${cols}) unique: rows already repeat a value\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + listed + ` PostgreSQL will not build "${failure.index}" while two rows share a\n value, and Atlas applies a push as one transaction — ${chalk.yellow("nothing in")}\n ${chalk.yellow("this push was applied.")}\n\n Find them:\n ${chalk.gray(`SELECT ${cols}, count(*) FROM ${table} GROUP BY ${cols} HAVING count(*) > 1;`)}\n\n Then change or delete the repeats and push again, or take ${chalk.bold("unique")} off\n the property.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1844
+ }
1845
+ /** How many rows a table holds, or `null` when we cannot say. */
1846
+ async function countRows(databaseUrl, table) {
1847
+ if (!databaseUrl) return null;
1848
+ try {
1849
+ const { Client } = await import("pg");
1850
+ const client = new Client({
1851
+ connectionString: databaseUrl,
1852
+ connectionTimeoutMillis: 5e3
1853
+ });
1854
+ await client.connect();
1855
+ try {
1856
+ return (await client.query(`SELECT count(*)::int AS n FROM "${table.replace(/"/g, "\"\"")}"`)).rows[0]?.n ?? null;
1857
+ } finally {
1858
+ await client.end();
1859
+ }
1860
+ } catch {
1861
+ return null;
1862
+ }
1863
+ }
1864
+ /**
1865
+ * A hand-written generated column reading a column the plan wants to retype or
1866
+ * drop.
1867
+ *
1868
+ * Rebase's own generated columns are dropped and rebuilt around the apply — it
1869
+ * holds their definition in `search.sql`. This one it does not: PostgreSQL
1870
+ * stores a generated expression only in the catalogue, so dropping it to let
1871
+ * the apply through would destroy the only copy. Refuse instead, and say what
1872
+ * the operator can do about it.
1873
+ */
1874
+ function formatForeignGeneratedColumnBanner(conflicts) {
1875
+ const lines = conflicts.map((c) => {
1876
+ const reads = c.blocking.map((b) => `"${b.column}"`).join(", ");
1877
+ return ` ${chalk.bold(`${c.schema}.${c.table}.${c.column}`)} reads ${reads}`;
1878
+ });
1879
+ const first = conflicts[0];
1880
+ return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ A generated column depends on a column this push changes\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + lines.join("\n") + `
1881
+
1882
+ PostgreSQL refuses to retype or drop a column while a GENERATED
1883
+ ALWAYS AS … STORED expression reads it, and an Atlas apply is one
1884
+ transaction — so this refusal would roll back every other statement
1885
+ in the push with it.
1886
+
1887
+ ${chalk.yellow("Nothing was applied.")} Rebase rebuilds the generated columns it made\n itself (a collection's ${chalk.bold("search")} block) around the apply. It has no\n definition for this one, and a dropped generated expression is not\n recoverable from anywhere but your own migration.\n\n Drop it, push, and put it back:\n\n ${chalk.gray(`ALTER TABLE "${first.schema}"."${first.table}" DROP COLUMN "${first.column}";`)}\n ${chalk.gray("rebase db push")}\n ${chalk.gray("-- then re-create the column with its original expression")}\n\n Or declare it through a ${chalk.bold("search")} block, and Rebase will own it.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
1888
+ }
1889
+ /**
1890
+ * The remedy for an Atlas invocation that failed, or `null`.
1891
+ *
1892
+ * Scoped by the invocation, because the same database state means different
1893
+ * things to different subcommands: "already exists" under `migrate apply` wants
1894
+ * a baseline, while under `schema apply` it is a genuine conflict.
1895
+ */
1896
+ async function diagnoseAtlasFailure(context) {
1897
+ const { domain, args, stderr, databaseUrl } = context;
1898
+ if (domain === "migrate" && args.includes("apply")) {
1899
+ const provisioned = parseAlreadyProvisioned(stderr);
1900
+ if (provisioned) return formatBaselineRemedy(context.latestMigrationVersion ?? null, provisioned.object);
1901
+ }
1902
+ if (domain === "schema" && args.includes("apply")) {
1903
+ const enumDrop = parseEnumLabelDrop(stderr);
1904
+ if (enumDrop) return formatEnumLabelDropBanner(enumDrop);
1905
+ const notNull = parseNotNullViolation(stderr);
1906
+ if (notNull) return formatNotNullViolationBanner(notNull, await countRows(databaseUrl, notNull.table), notNullColumnIsNew(stderr, notNull.column));
1907
+ const uncastable = parseUncastableRetype(stderr);
1908
+ if (uncastable) return formatUncastableRetypeBanner(uncastable);
1909
+ const unique = parseUniqueBuildFailure(stderr);
1910
+ if (unique) return formatUniqueBuildFailureBanner(unique, await duplicateValues(databaseUrl, unique.statement ? failedStatementTable(unique.statement) : null, unique.columns));
1911
+ const generated = parseGeneratedColumnRefusal(stderr);
1912
+ if (generated) return formatGeneratedColumnRefusalBanner(generated, await generatedColumnsBlocking(databaseUrl, generated.statement));
1913
+ }
1914
+ return null;
1915
+ }
1916
+ //#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 };
1918
+
1919
+ //# sourceMappingURL=cli-errors-CaI4ntyD.js.map