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