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