@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
@@ -1,893 +0,0 @@
1
- import { createRequire as __createRequire } from "module";
2
- __createRequire(import.meta.url);
3
- import { n as outError, r as outWarn } from "./cli-output-CNdMql-L.js";
4
- import { t as chalk } from "./source-Br7L7GOI.js";
5
- import { declaredErrorAnswer, logger } from "@rebasepro/server";
6
- import { fieldKeyForColumn, getTableName } from "@rebasepro/common";
7
- //#region src/utils/pg-error-utils.ts
8
- /**
9
- * Shared PostgreSQL error extraction and user-friendly message formatting.
10
- *
11
- * Drizzle wraps native PG errors in a `.cause` chain. These utilities
12
- * unwrap that chain to get the real PostgreSQL error (identified by a
13
- * 5-character alphanumeric `code` such as `42P01`) and translate it into
14
- * a message that is safe and helpful to show to end-users.
15
- */
16
- /**
17
- * Return the error when it is a deliberate 4xx, otherwise null.
18
- *
19
- * A thrown `ApiError` is a decision the server made about the request, not a
20
- * database failure: its message and code are already written for the client.
21
- *
22
- * So is a `RebaseApiError` — what a collection callback throws, since a
23
- * collection file cannot import the server package. It spells its status
24
- * `status`, and reading only `statusCode` here turned an `afterRead` that
25
- * refused with a 403 into "Could not load data … Check server logs" on a
26
- * subscription, where REST and the socket's own requests answer the 403.
27
- * Recognised by the predicate those doors use, `declaredErrorAnswer`.
28
- */
29
- function asClientFacingError(error) {
30
- const answer = declaredErrorAnswer(error);
31
- if (answer) {
32
- if (answer.status < 400 || answer.status >= 500) return null;
33
- return {
34
- statusCode: answer.status,
35
- code: answer.code,
36
- message: answer.message,
37
- expected: answer.expected
38
- };
39
- }
40
- if (!(error instanceof Error)) return null;
41
- const e = error;
42
- if (typeof e.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500) return null;
43
- return e;
44
- }
45
- /**
46
- * Extract the underlying PostgreSQL error from a Drizzle wrapper.
47
- * Drizzle wraps PG errors in a `cause` property — this function
48
- * recursively walks the chain until it finds an object with a PG
49
- * error code (5-char alphanumeric, e.g. `42P01`).
50
- */
51
- function extractPgError(error) {
52
- if (!error || typeof error !== "object") return null;
53
- if (!(error instanceof Error)) {
54
- if ("cause" in error && error.cause && typeof error.cause === "object") return extractPgError(error.cause);
55
- return null;
56
- }
57
- if ("code" in error && typeof error.code === "string" && /^[0-9A-Z]{5}$/.test(error.code)) return error;
58
- if (error.cause && typeof error.cause === "object") return extractPgError(error.cause);
59
- return null;
60
- }
61
- /**
62
- * Whether the failure came back from Postgres rather than from building the
63
- * query — which decides whether a fallback query is worth issuing.
64
- *
65
- * Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
66
- * RLS). Once a statement raises, that transaction is aborted, and every later
67
- * statement on it returns `25P02` — "current transaction is aborted, commands
68
- * ignored until end of transaction block". So a retry after a database error
69
- * cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
70
- * for type uuid") with a generic one. Rethrow instead.
71
- *
72
- * A query the driver could not even build — a missing reciprocal relation, say
73
- * — never reached Postgres, leaves the transaction usable, and is exactly what
74
- * the fallback paths exist for.
75
- */
76
- function reachedDatabase(error) {
77
- return extractPgError(error) !== null;
78
- }
79
- /**
80
- * Walk the error cause chain and return the deepest meaningful message.
81
- */
82
- function extractCauseMessage(error) {
83
- if (!error || typeof error !== "object") return null;
84
- if (!(error instanceof Error)) return null;
85
- if (error.cause && typeof error.cause === "object") {
86
- const deeper = extractCauseMessage(error.cause);
87
- if (deeper) return deeper;
88
- if (error.cause instanceof Error && error.cause.message) return error.cause.message;
89
- }
90
- return null;
91
- }
92
- /**
93
- * Codes that mean "this connection will never work as configured".
94
- *
95
- * A wrong password or a database that does not exist is a settled fact about
96
- * the connection string, not a transient fault — retrying produces the same
97
- * answer forever.
98
- */
99
- var UNRECOVERABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
100
- "28P01",
101
- "28000",
102
- "3D000",
103
- "42501"
104
- ]);
105
- /**
106
- * Describe a failed connection attempt in terms a developer can act on.
107
- *
108
- * The error a caller catches is Drizzle's wrapper: its message is
109
- * `Failed query: SELECT 1` and its stack runs through drizzle internals, while
110
- * the sentence that says what is actually wrong — "password authentication
111
- * failed for user …", "database … does not exist" — sits in `.cause`. Logging
112
- * the wrapper, as the bootstrapper used to, tells a developer with a typo in
113
- * their `DATABASE_URL` nothing at all.
114
- */
115
- function classifyConnectFailure(error) {
116
- const pgError = extractPgError(error);
117
- const reason = pgError?.message ?? extractCauseMessage(error) ?? (error instanceof Error ? error.message : String(error));
118
- return {
119
- fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
120
- reason,
121
- code: pgError?.code
122
- };
123
- }
124
- /**
125
- * Detect whether an error is specifically a role-switching permission failure
126
- * (e.g. "permission denied to set role" or "must be member of role"),
127
- * as opposed to a table-level permission denial.
128
- *
129
- * This is used by the backend driver to auto-disable role switching when the
130
- * connection user lacks SET ROLE privileges, rather than surfacing a confusing
131
- * error to the Studio SQL Editor user.
132
- */
133
- function isRoleSwitchingPermissionError(error) {
134
- const pgError = extractPgError(error);
135
- if (!pgError || pgError.code !== "42501") return false;
136
- const msg = pgError.message.toLowerCase();
137
- return msg.includes("set role") || msg.includes("member of role");
138
- }
139
- /**
140
- * Was this `42501` the *caller* being refused by a policy, rather than the
141
- * server lacking a privilege?
142
- *
143
- * Both arrive as `insufficient_privilege`, and they are opposite kinds of
144
- * problem. A row-level-security refusal is a working access-control system
145
- * doing its job: the caller asked for something their policies do not permit,
146
- * which is a 403 and nobody's bug. A missing `GRANT` is the deployment being
147
- * wrong — the connection role cannot touch the table at all, no policy is
148
- * involved, and nothing the caller changes about the request will help.
149
- *
150
- * Postgres distinguishes them in the message, so this does too:
151
- *
152
- * new row violates row-level security policy for table "notes" → the caller
153
- * permission denied for table notes → the server
154
- *
155
- * Only writes reach this. A read that RLS excludes is not an error — the rows
156
- * are filtered and the caller gets an empty page — so the erroring case is
157
- * specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.
158
- *
159
- * Matched on the message because that is the only thing carrying the
160
- * distinction; the SQLSTATE is identical either way. Narrow by design: anything
161
- * not naming row-level security stays the server's problem, since reporting a
162
- * genuine privilege misconfiguration as "forbidden" would send an operator
163
- * hunting for a policy bug that does not exist.
164
- */
165
- function isRowLevelSecurityDenial(error) {
166
- const pgError = extractPgError(error);
167
- if (!pgError || pgError.code !== "42501") return false;
168
- return pgError.message.toLowerCase().includes("row-level security policy");
169
- }
170
- /**
171
- * The physical column names a Postgres error is about.
172
- *
173
- * Postgres puts one in `column` for `23502` and nowhere else, so the rest are
174
- * read out of the two places that do carry them:
175
- *
176
- * - `detail` — `Key (author_id)=(9) is not present in table "authors".` and
177
- * `Key (email, tenant_id)=(…) already exists.` Only the parenthesised column
178
- * list is read; the values after `=` are never touched.
179
- * - `constraint` — `users_email_key`, `posts_author_id_fkey`. The table prefix
180
- * and the constraint suffix are stripped, leaving the columns joined by `_`,
181
- * which is ambiguous for multi-column constraints and is therefore only used
182
- * when `detail` gave nothing.
183
- *
184
- * `22P02` carries neither, because an invalid literal fails during parse rather
185
- * than against a constraint. Its message names the *type*, and an enum type is
186
- * generated as `<table>_<column>` — which is the one case worth decoding,
187
- * because a value outside an enum's labels is the most common 22P02 anyone
188
- * hits and the least legible one.
189
- */
190
- function offendingColumns(pgError, collection) {
191
- if (pgError.column) return [pgError.column];
192
- const detail = pgError.detail;
193
- if (detail) {
194
- const keyed = /Key \(([^)]+)\)=/.exec(detail);
195
- if (keyed) return keyed[1].split(",").map((part) => part.trim().replace(/^"|"$/g, "")).filter(Boolean);
196
- }
197
- if (pgError.code === "22P02") {
198
- const enumType = /invalid input value for enum ([^:\s]+)/i.exec(pgError.message ?? "");
199
- if (enumType && collection) {
200
- const typeName = enumType[1].replace(/^.*\./, "").replace(/"/g, "");
201
- const table = getTableName(collection);
202
- if (typeName.startsWith(`${table}_`)) return [typeName.slice(table.length + 1)];
203
- }
204
- return [];
205
- }
206
- const constraint = pgError.constraint;
207
- if (constraint && collection) {
208
- const table = getTableName(collection);
209
- const stripped = constraint.replace(new RegExp(`^${table}_`), "").replace(/_(pkey|key|fkey|unique|check|excl|idx)$/, "");
210
- if (stripped && stripped !== constraint) {
211
- const columns = /* @__PURE__ */ new Set();
212
- for (const [key, prop] of Object.entries(collection.properties ?? {})) columns.add(prop?.columnName ?? key);
213
- if (columns.has(stripped)) return [stripped];
214
- const wire = fieldKeyForColumn(collection, stripped);
215
- if (collection.properties && wire in collection.properties) return [stripped];
216
- }
217
- }
218
- return [];
219
- }
220
- /**
221
- * The fields a Postgres error is about, for `details.violations`.
222
- *
223
- * Only the four SQLSTATEs that describe *the caller's data*: a missing value, a
224
- * duplicate, a bad literal, a foreign key pointing at nothing. Everything else
225
- * — a dropped connection, a missing table, a privilege problem — is the
226
- * deployment's, and naming a field for it would send the reader to the wrong
227
- * place.
228
- *
229
- * Empty when the collection is unknown or the column cannot be recovered. An
230
- * empty list is the honest answer; inventing a field is worse than saying
231
- * nothing, because a form will mark the wrong input red.
232
- */
233
- function pgFieldViolations(pgError, collection) {
234
- const code = pgError.code;
235
- if (code !== "23502" && code !== "23505" && code !== "22P02" && code !== "23503") return [];
236
- if (!collection) return [];
237
- const columns = offendingColumns(pgError, collection);
238
- if (columns.length === 0) return [];
239
- return columns.map((column) => ({
240
- field: fieldKeyForColumn(collection, column),
241
- code
242
- }));
243
- }
244
- /**
245
- * Translate a raw PostgreSQL error into a user-friendly message.
246
- *
247
- * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
248
- * @param context - A human-readable context string (e.g. collection slug or path)
249
- * @returns An object with a `message` safe for the client and the PG `code`.
250
- */
251
- function pgErrorToFriendlyMessage(pgError, context, options = {}) {
252
- const verbose = options.verbose ?? process.env.NODE_ENV !== "production";
253
- const detail = verbose ? pgError.detail : void 0;
254
- const hint = verbose ? pgError.hint : void 0;
255
- const constraint = pgError.constraint;
256
- const column = pgError.column;
257
- const table = pgError.table;
258
- const dataType = verbose ? pgError.dataType : void 0;
259
- const rawMessage = pgError.message || "Unknown database error";
260
- const pgMessage = verbose ? rawMessage : "";
261
- const code = pgError.code || "UNKNOWN";
262
- const violations = pgFieldViolations(pgError, options.collection);
263
- const suffix = hint ? ` Hint: ${hint}` : "";
264
- const tableRef = table ?? context;
265
- /** `": <postgres said>"`, or nothing when the details are withheld. */
266
- const said = pgMessage ? `: ${pgMessage}` : "";
267
- switch (pgError.code) {
268
- case "23503": return {
269
- message: detail ? `Foreign key constraint violated: ${detail}${suffix}` : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
270
- code,
271
- violations
272
- };
273
- case "23001": return {
274
- message: `Cannot complete operation: rows in "${table ?? "another table"}" still reference this row in "${context}", and their foreign key${constraint ? ` (${constraint})` : ""} does not allow it to be deleted or its key changed. Delete or reassign those rows first.${detail ? ` ${detail}` : ""}${suffix}`,
275
- code,
276
- violations
277
- };
278
- case "23505": return {
279
- message: detail ? `Duplicate value: ${detail}${suffix}` : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
280
- code,
281
- violations
282
- };
283
- case "23502": return {
284
- message: `Missing required field: "${violations[0]?.field ?? column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
285
- code,
286
- violations
287
- };
288
- case "23514": return {
289
- message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
290
- code,
291
- violations
292
- };
293
- case "22P02": return {
294
- message: `Invalid data format in "${context}"${said}.${suffix}`,
295
- code,
296
- violations
297
- };
298
- case "22001": return {
299
- message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}"${said}.${suffix}`,
300
- code,
301
- violations
302
- };
303
- case "22003": return {
304
- message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}"${said}.${suffix}`,
305
- code,
306
- violations
307
- };
308
- case "42703": return {
309
- message: `Unknown column in "${tableRef}"${said}. Check if your schema is up to date (run migrations).${suffix}`,
310
- code,
311
- violations
312
- };
313
- case "42P01": return {
314
- message: `Table not found for "${context}"${said}. Check if your schema is up to date (run migrations).${suffix}`,
315
- code,
316
- violations
317
- };
318
- case "42501": return rawMessage.toLowerCase().includes("row-level security policy") ? {
319
- message: `Not permitted to write this row in "${tableRef}": it does not satisfy the row-level security policy.${suffix}`,
320
- code,
321
- violations
322
- } : {
323
- message: `Permission denied on "${tableRef}": the database role this server connects as lacks privileges on it.${suffix}`,
324
- code,
325
- violations
326
- };
327
- case "28000": return {
328
- message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
329
- code,
330
- violations
331
- };
332
- default: {
333
- const parts = [`Database error in "${context}" [${code}]${said}`];
334
- if (detail) parts.push(`Detail: ${detail}`);
335
- if (column) parts.push(`Column: ${column}`);
336
- if (dataType) parts.push(`Data type: ${dataType}`);
337
- if (constraint) parts.push(`Constraint: ${constraint}`);
338
- if (hint) parts.push(`Hint: ${hint}`);
339
- return {
340
- message: parts.join(". "),
341
- code,
342
- violations
343
- };
344
- }
345
- }
346
- }
347
- /**
348
- * Sanitize any error into a message safe and helpful for the client.
349
- *
350
- * A deliberate 4xx (`ApiError`) passes through untouched — the server already
351
- * decided what the client should read. Otherwise the PG error is extracted
352
- * from the Drizzle cause chain, falling back to a generic message that
353
- * doesn't leak SQL.
354
- *
355
- * @param error - The raw caught error
356
- * @param context - A human-readable context string (e.g. collection path)
357
- * @returns An object with `message` (user-friendly) and optional `code`
358
- * (the `ApiError` code, or the PG SQLSTATE).
359
- */
360
- function sanitizeErrorForClient(error, context, options = {}) {
361
- const clientError = asClientFacingError(error);
362
- if (clientError) {
363
- const line = `[API ${clientError.statusCode} ${clientError.code ?? "BAD_REQUEST"}] in "${context}": ${clientError.message}`;
364
- if (clientError.expected) logger.debug(line);
365
- else logger.warn(`⚠️ ${line}`);
366
- return {
367
- message: clientError.message,
368
- ...clientError.code && { code: clientError.code }
369
- };
370
- }
371
- const pgError = extractPgError(error);
372
- if (pgError) {
373
- logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
374
- code: pgError.code,
375
- message: pgError.message,
376
- detail: pgError.detail,
377
- hint: pgError.hint,
378
- column: pgError.column,
379
- table: pgError.table,
380
- constraint: pgError.constraint,
381
- dataType: pgError.dataType
382
- });
383
- const { message, code, violations } = pgErrorToFriendlyMessage(pgError, context, { collection: options.collection });
384
- return {
385
- message,
386
- code,
387
- ...violations.length > 0 && { violations }
388
- };
389
- }
390
- logger.error(`Database error in "${context}" (no PG error extracted)`, {
391
- error: error instanceof Error ? error.message : String(error),
392
- stack: error instanceof Error ? error.stack : void 0,
393
- cause: error instanceof Error && error.cause ? error.cause instanceof Error ? error.cause.message : String(error.cause) : void 0
394
- });
395
- const causeMessage = extractCauseMessage(error);
396
- if (causeMessage) return { message: `Database error in "${context}": ${causeMessage}` };
397
- return { message: `Could not load data for "${context}". Check server logs for details.` };
398
- }
399
- //#endregion
400
- //#region src/cli-errors.ts
401
- /**
402
- * Detect whether an error (or AggregateError wrapping multiple attempts)
403
- * represents an ECONNREFUSED — i.e. the database is simply not running.
404
- *
405
- * Handles:
406
- * - Direct `{ code: "ECONNREFUSED" }` errors from Node `net`
407
- * - `AggregateError` from dual-stack IPv4+IPv6 connection attempts
408
- * - Drizzle's `cause`-wrapped pg errors
409
- */
410
- function isEconnrefused(err) {
411
- if (!err || typeof err !== "object") return false;
412
- const e = err;
413
- if (e.code === "ECONNREFUSED") return true;
414
- if (Array.isArray(e.errors)) return e.errors.some((inner) => inner && typeof inner === "object" && inner.code === "ECONNREFUSED");
415
- if (e.cause && typeof e.cause === "object") return isEconnrefused(e.cause);
416
- return false;
417
- }
418
- /**
419
- * Detect PostgreSQL authentication failures.
420
- * PG error codes: 28P01 (invalid_password), 28000 (invalid_authorization_specification)
421
- */
422
- function isAuthFailure(err) {
423
- if (!err || typeof err !== "object") return false;
424
- const e = err;
425
- if (e.code === "28P01" || e.code === "28000") return true;
426
- if (e.cause && typeof e.cause === "object") return isAuthFailure(e.cause);
427
- if ("message" in e && typeof e.message === "string") {
428
- const msg = e.message.toLowerCase();
429
- if (msg.includes("password authentication failed") || msg.includes("no pg_hba.conf entry")) return true;
430
- }
431
- return false;
432
- }
433
- /**
434
- * Detect the "SSL is not enabled on the server" failure — the client attempted
435
- * an SSL handshake against a Postgres server that doesn't support it (common
436
- * with a plain local dev database). The fix is `?sslmode=disable` on the URL.
437
- */
438
- function isSslNotEnabled(err) {
439
- if (!err || typeof err !== "object") return false;
440
- const e = err;
441
- if (typeof e.message === "string" && e.message.toLowerCase().includes("ssl is not enabled on the server")) return true;
442
- if (e.cause && typeof e.cause === "object") return isSslNotEnabled(e.cause);
443
- return false;
444
- }
445
- /**
446
- * Detect PostgreSQL "cannot drop ... because other objects depend on it"
447
- * (error code 2BP01, dependent_objects_still_exist). This is the failure that
448
- * strands a declarative `db push` half-applied when a collection is removed but
449
- * an enum type it defined is still referenced by another object.
450
- */
451
- function isDependencyDropError(err) {
452
- if (!err || typeof err !== "object") return false;
453
- const e = err;
454
- if (e.code === "2BP01") return true;
455
- if (typeof e.message === "string") {
456
- const msg = e.message.toLowerCase();
457
- if (msg.includes("other objects depend on it") || msg.includes("cannot drop type")) return true;
458
- }
459
- if (e.cause && typeof e.cause === "object") return isDependencyDropError(e.cause);
460
- return false;
461
- }
462
- /**
463
- * Parse host:port from a DATABASE_URL for display purposes.
464
- *
465
- * Exported because every message about a connection has to name the thing it
466
- * could not reach, and the boot path needs the same rendering the CLI banners
467
- * use — including the same refusal to print the URL itself, which carries the
468
- * password.
469
- */
470
- function parseHostInfo(databaseUrl) {
471
- try {
472
- const parsed = new URL(databaseUrl);
473
- return `${parsed.hostname}:${parsed.port || 5432}`;
474
- } catch {
475
- return "unknown";
476
- }
477
- }
478
- /**
479
- * The sentence the operating system actually produced, dug out of the wrappers.
480
- *
481
- * `connect ECONNREFUSED 127.0.0.1:5432` is written by `net`, then wrapped by
482
- * `pg`, then wrapped again by Drizzle as `Failed query: …` — and on a
483
- * dual-stack host it is not in `.cause` at all but inside the
484
- * `AggregateError.errors` array of one attempt per resolved address. Printing
485
- * the banner without it loses the one token every search engine, runbook and
486
- * colleague recognises.
487
- */
488
- function deepestErrorMessage(err) {
489
- if (!err || typeof err !== "object") return null;
490
- const e = err;
491
- if (Array.isArray(e.errors)) for (const inner of e.errors) {
492
- const deeper = deepestErrorMessage(inner);
493
- if (deeper) return deeper;
494
- }
495
- if (e.cause) {
496
- const deeper = deepestErrorMessage(e.cause);
497
- if (deeper) return deeper;
498
- }
499
- if (typeof e.message === "string" && e.message && !e.message.startsWith("Failed query:")) return e.code ? `${e.message} (${e.code})` : e.message;
500
- return null;
501
- }
502
- /** One indented line naming the driver's own reason, or nothing. */
503
- function driverReasonLine(err) {
504
- const reason = err === void 0 ? null : deepestErrorMessage(err);
505
- return reason ? ` The driver said: ${reason}\n\n` : "";
506
- }
507
- /**
508
- * Format a diagnostic banner for ECONNREFUSED errors.
509
- */
510
- function formatConnectionRefusedBanner(databaseUrl, err) {
511
- return `
512
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
513
- ❌ 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";
514
- }
515
- /**
516
- * Format a diagnostic banner for authentication failures.
517
- */
518
- function formatAuthFailureBanner(databaseUrl, err) {
519
- const hostInfo = parseHostInfo(databaseUrl);
520
- let username = "unknown";
521
- try {
522
- username = new URL(databaseUrl).username || "unknown";
523
- } catch {}
524
- return `
525
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
526
- ❌ Authentication failed for user "${username}" at ${hostInfo}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + driverReasonLine(err) + ` PostgreSQL rejected the credentials. Common fixes:
527
-
528
- • Check the username and password in DATABASE_URL
529
- • Verify the user exists: psql -c "\\du"
530
- • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
531
- }
532
- /**
533
- * Format a diagnostic banner for "SSL is not enabled on the server".
534
- */
535
- function formatSslNotEnabledBanner(databaseUrl) {
536
- const hostInfo = parseHostInfo(databaseUrl);
537
- const suggestion = databaseUrl.includes("?") ? "&sslmode=disable" : "?sslmode=disable";
538
- return `
539
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
540
- ❌ 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`;
541
- }
542
- /**
543
- * Format a diagnostic banner for a dependency-drop failure during `db push`.
544
- * Explains that the database may be left partially migrated and how to recover.
545
- */
546
- function formatDependencyDropBanner() {
547
- return `
548
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
549
- ❌ Schema push failed: a type/table could not be dropped
550
- because other objects still depend on it.
551
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
552
-
553
- ${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`;
554
- }
555
- /**
556
- * Pre-flight check: verify that the database is reachable before running
557
- * a heavy subprocess (Atlas, migrations, etc.).
558
- *
559
- * Exits with code 1 and a friendly banner on known failure modes.
560
- * On unknown errors, logs a warning and allows the caller to proceed.
561
- */
562
- async function checkDatabaseConnectivity(databaseUrl) {
563
- let client;
564
- try {
565
- const { Client } = await import("pg");
566
- client = new Client({
567
- connectionString: databaseUrl,
568
- connectionTimeoutMillis: 5e3
569
- });
570
- await client.connect();
571
- await client.query("SELECT 1");
572
- } catch (err) {
573
- if (isEconnrefused(err)) {
574
- outError(formatConnectionRefusedBanner(databaseUrl, err));
575
- process.exit(1);
576
- }
577
- if (isAuthFailure(err)) {
578
- outError(formatAuthFailureBanner(databaseUrl, err));
579
- process.exit(1);
580
- }
581
- if (isSslNotEnabled(err)) {
582
- outError(formatSslNotEnabledBanner(databaseUrl));
583
- process.exit(1);
584
- }
585
- outWarn(chalk.yellow(` ⚠ Could not verify database connectivity: ${err instanceof Error ? err.message : String(err)}`));
586
- outWarn(chalk.gray(" Proceeding anyway — the command may fail if the database is unreachable."));
587
- } finally {
588
- try {
589
- await client?.end();
590
- } catch {}
591
- }
592
- }
593
- /**
594
- * Post-hoc error diagnosis for direct database operations (e.g. applyPolicies).
595
- * Returns a formatted diagnostic string if the error matches a known pattern,
596
- * or null if unrecognized.
597
- */
598
- function diagnoseDbError(err, databaseUrl) {
599
- if (isEconnrefused(err)) return formatConnectionRefusedBanner(databaseUrl || "", err);
600
- if (isAuthFailure(err)) return formatAuthFailureBanner(databaseUrl || "", err);
601
- if (isSslNotEnabled(err)) return formatSslNotEnabledBanner(databaseUrl || "");
602
- if (isDependencyDropError(err)) return formatDependencyDropBanner();
603
- return null;
604
- }
605
- /**
606
- * Say why the command failed, on the way out.
607
- *
608
- * The entry point below used to be `.catch(() => process.exit(1))`, which threw
609
- * the error away. Every message this file and its services raise — "Branch
610
- * \"x\" already exists.", "the source database has active connections", "Branch
611
- * name is too long" — was written, wrapped in the right PG error code, and then
612
- * discarded one frame before it reached a terminal. What a developer saw was a
613
- * header line, no error, and exit 1.
614
- *
615
- * Two shapes are deliberately kept quiet:
616
- *
617
- * - **A child process that already spoke.** Atlas, `pg_dump` and `psql` run
618
- * with inherited stdio, so their diagnosis is on the terminal already and
619
- * execa's wrapper adds only `Command failed with exit code 1: atlas …`.
620
- * `packages/cli` filters exactly these two phrasings one level up
621
- * (`runDbCommand`), and this is that filter, for the process that is actually
622
- * throwing.
623
- *
624
- * - **A message that is only a query.** Drizzle reports failures as
625
- * `Failed query: <sql> params:` and hides the real PostgreSQL error in
626
- * `cause`, so the wrapper alone tells a reader nothing they can act on. The
627
- * cause is appended when it says something the message does not.
628
- *
629
- * - **An error that has already printed its own diagnosis**, marked
630
- * `alreadyReported`. `CollectionsPathMissing` is one: it prints the path, what
631
- * it resolved to and the cwd it resolved against, and then throws so the entry
632
- * point owns the exit code. Repeating its one-line summary underneath would
633
- * undo the "printed once" this whole path exists for.
634
- */
635
- function reportCommandFailure(error) {
636
- const message = error instanceof Error ? error.message : String(error ?? "");
637
- if (error?.alreadyReported) return;
638
- if (!message || /Command failed|exited with code/i.test(message)) return;
639
- outError("");
640
- outError(chalk.red(` ✗ ${message}`));
641
- const cause = extractCauseMessage(error);
642
- if (cause && !message.includes(cause)) outError(chalk.gray(` ${cause}`));
643
- outError("");
644
- }
645
- /**
646
- * `migrate apply` refusing because the database already has the schema.
647
- *
648
- * `42710` (duplicate_object) and `42P07` (duplicate_table) are what a migration
649
- * hits when boot-ensure — or a `db push` — has already provisioned the objects
650
- * it was going to create. Since boot-ensure provisions *every* production
651
- * database, this is the normal case rather than the exotic one, and the raw
652
- * failure (`pq: type "posts_status" already exists (42710)`, then `sql/migrate:
653
- * write revision: … current transaction is aborted`) names no way forward.
654
- */
655
- function parseAlreadyProvisioned(text) {
656
- const withCode = /(?:type|relation|constraint|schema) "([^"]+)" already exists \((42710|42P07)\)/.exec(text);
657
- if (withCode) return {
658
- object: withCode[1],
659
- code: withCode[2]
660
- };
661
- const bare = /(?:type|relation|constraint|schema) "([^"]+)" already exists/.exec(text);
662
- if (bare && /42710|42P07/.test(text)) return {
663
- object: bare[1],
664
- code: /42P07/.test(text) ? "42P07" : "42710"
665
- };
666
- return null;
667
- }
668
- /**
669
- * What to do about it: tell Atlas the database is already at a version.
670
- *
671
- * Atlas's own mechanism (`migrate apply --baseline <version>`), not a ledger of
672
- * ours — it writes the revision row Atlas reads, so every later `rebase db
673
- * migrate` is an ordinary one.
674
- */
675
- function formatBaselineRemedy(version, object) {
676
- const versionToken = version ?? "<version>";
677
- 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`;
678
- }
679
- /** `ALTER TABLE … SET NOT NULL` on a table that already holds rows (23502). */
680
- function parseNotNullViolation(text) {
681
- const match = /column "([^"]+)" of relation "([^"]+)" contains null values/.exec(text);
682
- return match ? {
683
- table: match[2],
684
- column: match[1]
685
- } : null;
686
- }
687
- /**
688
- * The three ways out, because there is no fourth.
689
- *
690
- * Boot-ensure handles this case — it adds the column nullable and sets NOT NULL
691
- * only when the table is empty — so a push that dies here is strictly worse
692
- * than the boot that would have run instead, and the developer deserves to be
693
- * told which of the three they want rather than left with `pq: … contains null
694
- * values (23502)` and no next step.
695
- */
696
- function formatNotNullViolationBanner(violation, rowCount) {
697
- const { table, column } = violation;
698
- 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`;
699
- return `
700
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
701
- ❌ 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.
702
-
703
- Three ways forward:
704
-
705
- 1. Give the property a default, so the push can backfill:
706
- ${chalk.gray(`{ type: "string", defaultValue: "…", validation: { required: true } }`)}\n 2. Backfill by hand first, then push again:\n ${chalk.gray(`psql "$DATABASE_URL" -c 'UPDATE "${table}" SET "${column}" = … WHERE "${column}" IS NULL;'`)}\n 3. Leave it optional — take \`validation.required\` off the property.\n\n ${chalk.gray("Nothing was applied for this column. `rebase dev` would have added it")}\n ${chalk.gray("nullable instead: boot sets NOT NULL only when it is safe.")}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
707
- }
708
- /** Atlas refusing to remove a label from an enum — a renamed `enum` option id. */
709
- function parseEnumLabelDrop(text) {
710
- const match = /dropping (?:enum )?value "([^"]+)" from enum "([^"]+)" is not supported/.exec(text);
711
- return match ? {
712
- label: match[1],
713
- enumType: match[2]
714
- } : null;
715
- }
716
- /**
717
- * Why the two paths disagree, and what retiring an option id actually costs.
718
- *
719
- * Boot-ensure adds enum labels and never removes one, so `rebase dev` accepts
720
- * the very edit that stops `db push` dead. PostgreSQL has no `ALTER TYPE …
721
- * DROP VALUE`: a label goes only by rewriting the type, which is a data
722
- * migration and not a schema push.
723
- */
724
- function formatEnumLabelDropBanner(drop) {
725
- const { label, enumType } = drop;
726
- return `
727
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
728
- ❌ "${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`;
729
- }
730
- /**
731
- * The statement Atlas was running when it failed, unescaped.
732
- *
733
- * Two spellings, one per phase: the apply quotes it
734
- * (`executing statement "ALTER TABLE \"public\".\"people\" …;": pq: …`), and
735
- * the dry-run's analysis does not
736
- * (`analyzing plan: executing statement: ALTER TABLE "public"."people" …;: pq: …`).
737
- */
738
- function parseFailedStatement(text) {
739
- const quoted = /executing statement "((?:[^"\\]|\\.)*)"/.exec(text);
740
- if (quoted) return quoted[1].replace(/\\(.)/g, "$1");
741
- const bare = /executing statement: ([\s\S]*?;): pq:/.exec(text);
742
- return bare ? bare[1] : null;
743
- }
744
- /** `ALTER TABLE "public"."people" …` → `{ schema: "public", table: "people" }`. */
745
- function failedStatementTable(statement) {
746
- const ident = String.raw`(?:"((?:[^"]|"")+)"|([A-Za-z_][A-Za-z0-9_$]*))`;
747
- 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);
748
- if (!match) return null;
749
- const first = (match[1] ?? match[2]).replace(/""/g, "\"");
750
- const second = match[3] ?? match[4];
751
- return second === void 0 ? { table: first } : {
752
- schema: first,
753
- table: second.replace(/""/g, "\"")
754
- };
755
- }
756
- /**
757
- * PostgreSQL refusing to retype a column a `GENERATED ALWAYS AS … STORED`
758
- * expression reads (0A000). `db push` moves Rebase's own search columns out of
759
- * the way before the apply; this is what is left when it could not.
760
- */
761
- function parseGeneratedColumnRefusal(text) {
762
- if (!/cannot alter type of a column used by a generated column/.test(text)) return null;
763
- return { statement: parseFailedStatement(text) };
764
- }
765
- /**
766
- * Which generated columns read a column the failed statement retypes — read
767
- * from the catalogue, because PostgreSQL's DETAIL naming them does not survive
768
- * Atlas.
769
- */
770
- async function generatedColumnsBlocking(databaseUrl, statement) {
771
- if (!databaseUrl || !statement) return [];
772
- const target = failedStatementTable(statement);
773
- if (!target) return [];
774
- const retyped = [...statement.matchAll(/ALTER\s+(?:COLUMN\s+)?"((?:[^"]|"")+)"\s+(?:SET\s+DATA\s+)?TYPE\b/gi)].map((match) => match[1].replace(/""/g, "\""));
775
- if (retyped.length === 0) return [];
776
- try {
777
- const { Client } = await import("pg");
778
- const client = new Client({
779
- connectionString: databaseUrl,
780
- connectionTimeoutMillis: 5e3
781
- });
782
- await client.connect();
783
- try {
784
- return (await client.query(`
785
- SELECT n.nspname AS "schema", c.relname AS "table", gen.attname AS "column", dep.attname AS "reads"
786
- FROM pg_attrdef ad
787
- JOIN pg_class c ON c.oid = ad.adrelid
788
- JOIN pg_namespace n ON n.oid = c.relnamespace
789
- JOIN pg_attribute gen ON gen.attrelid = ad.adrelid AND gen.attnum = ad.adnum
790
- JOIN pg_depend d ON d.classid = 'pg_attrdef'::regclass AND d.objid = ad.oid
791
- AND d.refclassid = 'pg_class'::regclass AND d.refobjid = ad.adrelid
792
- JOIN pg_attribute dep ON dep.attrelid = ad.adrelid AND dep.attnum = d.refobjsubid
793
- WHERE gen.attgenerated <> '' AND c.relname = $1
794
- AND ($2::text IS NULL OR n.nspname = $2) AND dep.attname = ANY($3::text[])
795
- ORDER BY 1, 2, 3, 4`, [
796
- target.table,
797
- target.schema ?? null,
798
- retyped
799
- ])).rows;
800
- } finally {
801
- await client.end();
802
- }
803
- } catch {
804
- return [];
805
- }
806
- }
807
- /**
808
- * What to do when a generated column stops a retype.
809
- *
810
- * A collection's own search column should never reach this — `db push` drops
811
- * it before the apply and rebuilds it after — so seeing one named here means
812
- * that machinery missed it, which is a bug worth a report. Any other generated
813
- * column is somebody's, with an expression Rebase holds no copy of.
814
- */
815
- function formatGeneratedColumnRefusalBanner(refusal, blockers) {
816
- 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` : "";
817
- const first = blockers[0];
818
- const drop = first ? `ALTER TABLE "${first.schema}"."${first.table}" DROP COLUMN "${first.column}";` : "ALTER TABLE … DROP COLUMN <the generated column>;";
819
- 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 …
820
- STORED expression reads it, and Atlas applies a push as one
821
- 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`;
822
- }
823
- /** How many rows a table holds, or `null` when we cannot say. */
824
- async function countRows(databaseUrl, table) {
825
- if (!databaseUrl) return null;
826
- try {
827
- const { Client } = await import("pg");
828
- const client = new Client({
829
- connectionString: databaseUrl,
830
- connectionTimeoutMillis: 5e3
831
- });
832
- await client.connect();
833
- try {
834
- return (await client.query(`SELECT count(*)::int AS n FROM "${table.replace(/"/g, "\"\"")}"`)).rows[0]?.n ?? null;
835
- } finally {
836
- await client.end();
837
- }
838
- } catch {
839
- return null;
840
- }
841
- }
842
- /**
843
- * A hand-written generated column reading a column the plan wants to retype or
844
- * drop.
845
- *
846
- * Rebase's own generated columns are dropped and rebuilt around the apply — it
847
- * holds their definition in `search.sql`. This one it does not: PostgreSQL
848
- * stores a generated expression only in the catalogue, so dropping it to let
849
- * the apply through would destroy the only copy. Refuse instead, and say what
850
- * the operator can do about it.
851
- */
852
- function formatForeignGeneratedColumnBanner(conflicts) {
853
- const lines = conflicts.map((c) => {
854
- const reads = c.blocking.map((b) => `"${b.column}"`).join(", ");
855
- return ` ${chalk.bold(`${c.schema}.${c.table}.${c.column}`)} reads ${reads}`;
856
- });
857
- const first = conflicts[0];
858
- return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ A generated column depends on a column this push changes\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + lines.join("\n") + `
859
-
860
- PostgreSQL refuses to retype or drop a column while a GENERATED
861
- ALWAYS AS … STORED expression reads it, and an Atlas apply is one
862
- transaction — so this refusal would roll back every other statement
863
- in the push with it.
864
-
865
- ${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`;
866
- }
867
- /**
868
- * The remedy for an Atlas invocation that failed, or `null`.
869
- *
870
- * Scoped by the invocation, because the same database state means different
871
- * things to different subcommands: "already exists" under `migrate apply` wants
872
- * a baseline, while under `schema apply` it is a genuine conflict.
873
- */
874
- async function diagnoseAtlasFailure(context) {
875
- const { domain, args, stderr, databaseUrl } = context;
876
- if (domain === "migrate" && args.includes("apply")) {
877
- const provisioned = parseAlreadyProvisioned(stderr);
878
- if (provisioned) return formatBaselineRemedy(context.latestMigrationVersion ?? null, provisioned.object);
879
- }
880
- if (domain === "schema" && args.includes("apply")) {
881
- const enumDrop = parseEnumLabelDrop(stderr);
882
- if (enumDrop) return formatEnumLabelDropBanner(enumDrop);
883
- const notNull = parseNotNullViolation(stderr);
884
- if (notNull) return formatNotNullViolationBanner(notNull, await countRows(databaseUrl, notNull.table));
885
- const generated = parseGeneratedColumnRefusal(stderr);
886
- if (generated) return formatGeneratedColumnRefusalBanner(generated, await generatedColumnsBlocking(databaseUrl, generated.statement));
887
- }
888
- return null;
889
- }
890
- //#endregion
891
- export { formatForeignGeneratedColumnBanner as a, reportCommandFailure as c, extractPgError as d, isRoleSwitchingPermissionError as f, sanitizeErrorForClient as g, reachedDatabase as h, diagnoseDbError as i, classifyConnectFailure as l, pgErrorToFriendlyMessage as m, deepestErrorMessage as n, isEconnrefused as o, isRowLevelSecurityDenial as p, diagnoseAtlasFailure as r, parseHostInfo as s, checkDatabaseConnectivity as t, extractCauseMessage as u };
892
-
893
- //# sourceMappingURL=cli-errors-DsA-K9uP.js.map