@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.gcb918fc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
- package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +91 -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 +14 -0
- 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-DnpJi6I7.js} +37 -29
- package/dist/backup-cli-DnpJi6I7.js.map +1 -0
- package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
- package/dist/backup-service-BTBB7RXn.js.map +1 -0
- package/dist/boot-ddl-DVpsFN6r.js +105 -0
- package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
- package/dist/cli-console-Ca6ZlAVU.js +154 -0
- package/dist/cli-console-Ca6ZlAVU.js.map +1 -0
- package/dist/cli-console.d.ts +39 -0
- package/dist/cli-errors-CaI4ntyD.js +1919 -0
- package/dist/cli-errors-CaI4ntyD.js.map +1 -0
- package/dist/cli-errors.d.ts +51 -5
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-JNI-pwSf.js} +6 -3
- package/dist/cli-flags-JNI-pwSf.js.map +1 -0
- package/dist/cli-helpers.d.ts +40 -0
- package/dist/cli-output.d.ts +16 -28
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli.js +566 -182
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
- package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C9mT8bgf.js} +24 -112
- package/dist/connection-C9mT8bgf.js.map +1 -0
- package/dist/connection.d.ts +13 -2
- package/dist/databasePoolManager-BIuewidb.js +119 -0
- package/dist/databasePoolManager-BIuewidb.js.map +1 -0
- package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
- package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
- package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
- package/dist/doctor-C4WiftHm.js.map +1 -0
- package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
- package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
- package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CVm_yvzb.js} +173 -77
- package/dist/ensure-tables-CVm_yvzb.js.map +1 -0
- package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
- package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
- package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
- package/dist/generate-files-CdEckX6c.js +199 -0
- package/dist/generate-files-CdEckX6c.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.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-C-Di8dA9.js} +44 -6
- package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
- package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
- package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
- package/dist/open-rebase-CbOur0TU.js +20858 -0
- package/dist/open-rebase-CbOur0TU.js.map +1 -0
- package/dist/open-rebase.d.ts +86 -0
- package/dist/pg-error-utils-vJezlXdI.js +388 -0
- package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
- package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
- package/dist/plan-schema-B1YF_tYq.js.map +1 -0
- package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
- package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
- package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
- package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
- package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
- package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
- package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
- package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
- package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
- package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
- package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +2 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- 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 +7 -0
- 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/introspect-db-logic.d.ts +12 -0
- package/dist/schema/introspect-db-queries.d.ts +11 -1
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +4 -5
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +126 -0
- 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/security/rls-enforcement.d.ts +32 -4
- 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-DBDbBTL4.js +811 -0
- package/dist/source-DBDbBTL4.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -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/websocket-BLGc5yhs.js +1604 -0
- package/dist/websocket-BLGc5yhs.js.map +1 -0
- package/dist/websocket.d.ts +13 -0
- package/package.json +7 -7
- 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
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { a as __toESM, i as __require, n as __commonJSMin, r as __exportAll
|
|
4
|
-
import { o as isRebaseIndexName } from "./collection-index-DdnsxiJ_.js";
|
|
5
|
-
import { r as outWarn, t as out } from "./cli-output-CNdMql-L.js";
|
|
6
|
-
import { t as chalk } from "./source-Br7L7GOI.js";
|
|
3
|
+
import { a as __toESM, i as __require, n as __commonJSMin, r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
7
4
|
import { LEGACY_RLS_SCHEMA, REBASE_SCHEMA } from "@rebasepro/types";
|
|
8
5
|
import { sqlRows } from "@rebasepro/common";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
6
|
+
import process$1, { execArgv, execPath, hrtime, platform } from "node:process";
|
|
7
|
+
import { constants } from "node:os";
|
|
8
|
+
import tty from "node:tty";
|
|
11
9
|
import { fileURLToPath } from "node:url";
|
|
12
10
|
import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
13
11
|
import { StringDecoder } from "node:string_decoder";
|
|
14
12
|
import { aborted, callbackify, debuglog, inspect, promisify, stripVTControlCharacters } from "node:util";
|
|
15
|
-
import
|
|
16
|
-
import tty from "node:tty";
|
|
17
|
-
import path$1 from "node:path";
|
|
18
|
-
import { execSync } from "child_process";
|
|
13
|
+
import path from "node:path";
|
|
19
14
|
import { scheduler, setImmediate, setTimeout } from "node:timers/promises";
|
|
20
|
-
import { constants } from "node:os";
|
|
21
15
|
import { EventEmitter, addAbortListener, on, once, setMaxListeners } from "node:events";
|
|
22
16
|
import { serialize } from "node:v8";
|
|
23
17
|
import { appendFileSync, createReadStream, createWriteStream, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
24
18
|
import { finished } from "node:stream/promises";
|
|
25
19
|
import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
|
|
26
20
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
27
|
-
import { createRequire } from "module";
|
|
28
|
-
import { fileURLToPath as fileURLToPath$1, pathToFileURL } from "url";
|
|
29
21
|
//#region src/utils/connection-string.ts
|
|
30
22
|
/**
|
|
31
23
|
* Connection-string rewrites for the tools Rebase shells out to.
|
|
@@ -851,7 +843,7 @@ var handleCommand = (filePath, rawArguments, rawOptions) => {
|
|
|
851
843
|
var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
852
844
|
module.exports = isexe;
|
|
853
845
|
isexe.sync = sync;
|
|
854
|
-
var fs$
|
|
846
|
+
var fs$2 = __require("fs");
|
|
855
847
|
function checkPathExt(path, options) {
|
|
856
848
|
var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
|
|
857
849
|
if (!pathext) return true;
|
|
@@ -868,12 +860,12 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
868
860
|
return checkPathExt(path, options);
|
|
869
861
|
}
|
|
870
862
|
function isexe(path, options, cb) {
|
|
871
|
-
fs$
|
|
863
|
+
fs$2.stat(path, function(er, stat) {
|
|
872
864
|
cb(er, er ? false : checkStat(stat, path, options));
|
|
873
865
|
});
|
|
874
866
|
}
|
|
875
867
|
function sync(path, options) {
|
|
876
|
-
return checkStat(fs$
|
|
868
|
+
return checkStat(fs$2.statSync(path), path, options);
|
|
877
869
|
}
|
|
878
870
|
}));
|
|
879
871
|
//#endregion
|
|
@@ -881,14 +873,14 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
881
873
|
var require_mode = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
882
874
|
module.exports = isexe;
|
|
883
875
|
isexe.sync = sync;
|
|
884
|
-
var fs$
|
|
876
|
+
var fs$1 = __require("fs");
|
|
885
877
|
function isexe(path, options, cb) {
|
|
886
|
-
fs$
|
|
878
|
+
fs$1.stat(path, function(er, stat) {
|
|
887
879
|
cb(er, er ? false : checkStat(stat, options));
|
|
888
880
|
});
|
|
889
881
|
}
|
|
890
882
|
function sync(path, options) {
|
|
891
|
-
return checkStat(fs$
|
|
883
|
+
return checkStat(fs$1.statSync(path), options);
|
|
892
884
|
}
|
|
893
885
|
function checkStat(stat, options) {
|
|
894
886
|
return stat.isFile() && checkMode(stat, options);
|
|
@@ -952,7 +944,7 @@ var require_isexe = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
952
944
|
//#region ../../node_modules/.pnpm/which@2.0.2/node_modules/which/which.js
|
|
953
945
|
var require_which = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
954
946
|
var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
|
|
955
|
-
var path$
|
|
947
|
+
var path$3 = __require("path");
|
|
956
948
|
var COLON = isWindows ? ";" : ":";
|
|
957
949
|
var isexe = require_isexe();
|
|
958
950
|
var getNotFoundError = (cmd) => Object.assign(/* @__PURE__ */ new Error(`not found: ${cmd}`), { code: "ENOENT" });
|
|
@@ -982,7 +974,7 @@ var require_which = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
982
974
|
if (i === pathEnv.length) return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
|
|
983
975
|
const ppRaw = pathEnv[i];
|
|
984
976
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
985
|
-
const pCmd = path$
|
|
977
|
+
const pCmd = path$3.join(pathPart, cmd);
|
|
986
978
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
987
979
|
resolve(subStep(p, i, 0));
|
|
988
980
|
});
|
|
@@ -1004,7 +996,7 @@ var require_which = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
1004
996
|
for (let i = 0; i < pathEnv.length; i++) {
|
|
1005
997
|
const ppRaw = pathEnv[i];
|
|
1006
998
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
1007
|
-
const pCmd = path$
|
|
999
|
+
const pCmd = path$3.join(pathPart, cmd);
|
|
1008
1000
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
1009
1001
|
for (let j = 0; j < pathExt.length; j++) {
|
|
1010
1002
|
const cur = p + pathExt[j];
|
|
@@ -1035,7 +1027,7 @@ var require_path_key = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
1035
1027
|
//#endregion
|
|
1036
1028
|
//#region ../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/resolveCommand.js
|
|
1037
1029
|
var require_resolveCommand = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1038
|
-
var path$
|
|
1030
|
+
var path$2 = __require("path");
|
|
1039
1031
|
var which = require_which();
|
|
1040
1032
|
var getPathKey = require_path_key();
|
|
1041
1033
|
function resolveCommandAttempt(parsed, withoutPathExt) {
|
|
@@ -1050,12 +1042,12 @@ var require_resolveCommand = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
1050
1042
|
try {
|
|
1051
1043
|
resolved = which.sync(parsed.command, {
|
|
1052
1044
|
path: env[getPathKey({ env })],
|
|
1053
|
-
pathExt: withoutPathExt ? path$
|
|
1045
|
+
pathExt: withoutPathExt ? path$2.delimiter : void 0
|
|
1054
1046
|
});
|
|
1055
1047
|
} catch (e) {} finally {
|
|
1056
1048
|
if (shouldSwitchCwd) process.chdir(cwd);
|
|
1057
1049
|
}
|
|
1058
|
-
if (resolved) resolved = path$
|
|
1050
|
+
if (resolved) resolved = path$2.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
|
|
1059
1051
|
return resolved;
|
|
1060
1052
|
}
|
|
1061
1053
|
function resolveCommand(parsed) {
|
|
@@ -1104,16 +1096,16 @@ var require_shebang_command = /* @__PURE__ */ __commonJSMin(((exports, module) =
|
|
|
1104
1096
|
//#endregion
|
|
1105
1097
|
//#region ../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js
|
|
1106
1098
|
var require_readShebang = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1107
|
-
var fs
|
|
1099
|
+
var fs = __require("fs");
|
|
1108
1100
|
var shebangCommand = require_shebang_command();
|
|
1109
1101
|
function readShebang(command) {
|
|
1110
1102
|
const size = 150;
|
|
1111
1103
|
const buffer = Buffer.alloc(size);
|
|
1112
1104
|
let fd;
|
|
1113
1105
|
try {
|
|
1114
|
-
fd = fs
|
|
1115
|
-
fs
|
|
1116
|
-
fs
|
|
1106
|
+
fd = fs.openSync(command, "r");
|
|
1107
|
+
fs.readSync(fd, buffer, 0, size, 0);
|
|
1108
|
+
fs.closeSync(fd);
|
|
1117
1109
|
} catch (e) {}
|
|
1118
1110
|
return shebangCommand(buffer.toString());
|
|
1119
1111
|
}
|
|
@@ -1122,7 +1114,7 @@ var require_readShebang = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
1122
1114
|
//#endregion
|
|
1123
1115
|
//#region ../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/parse.js
|
|
1124
1116
|
var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1125
|
-
var path$
|
|
1117
|
+
var path$1 = __require("path");
|
|
1126
1118
|
var resolveCommand = require_resolveCommand();
|
|
1127
1119
|
var escape = require_escape();
|
|
1128
1120
|
var readShebang = require_readShebang();
|
|
@@ -1145,7 +1137,7 @@ var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
1145
1137
|
const needsShell = !isExecutableRegExp.test(commandFile);
|
|
1146
1138
|
if (parsed.options.forceShell || needsShell) {
|
|
1147
1139
|
const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
|
|
1148
|
-
parsed.command = path$
|
|
1140
|
+
parsed.command = path$1.normalize(parsed.command);
|
|
1149
1141
|
parsed.command = escape.command(parsed.command);
|
|
1150
1142
|
parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
|
|
1151
1143
|
parsed.args = [
|
|
@@ -1254,33 +1246,33 @@ function toPath(urlOrPath) {
|
|
|
1254
1246
|
}
|
|
1255
1247
|
function traversePathUp(startPath) {
|
|
1256
1248
|
return { *[Symbol.iterator]() {
|
|
1257
|
-
let currentPath = path
|
|
1249
|
+
let currentPath = path.resolve(toPath(startPath));
|
|
1258
1250
|
let previousPath;
|
|
1259
1251
|
while (previousPath !== currentPath) {
|
|
1260
1252
|
yield currentPath;
|
|
1261
1253
|
previousPath = currentPath;
|
|
1262
|
-
currentPath = path
|
|
1254
|
+
currentPath = path.resolve(currentPath, "..");
|
|
1263
1255
|
}
|
|
1264
1256
|
} };
|
|
1265
1257
|
}
|
|
1266
1258
|
//#endregion
|
|
1267
1259
|
//#region ../../node_modules/.pnpm/npm-run-path@6.0.0/node_modules/npm-run-path/index.js
|
|
1268
1260
|
var npmRunPath = ({ cwd = process$1.cwd(), path: pathOption = process$1.env[pathKey()], preferLocal = true, execPath = process$1.execPath, addExecPath = true } = {}) => {
|
|
1269
|
-
const cwdPath = path
|
|
1261
|
+
const cwdPath = path.resolve(toPath(cwd));
|
|
1270
1262
|
const result = [];
|
|
1271
|
-
const pathParts = pathOption.split(path
|
|
1263
|
+
const pathParts = pathOption.split(path.delimiter);
|
|
1272
1264
|
if (preferLocal) applyPreferLocal(result, pathParts, cwdPath);
|
|
1273
1265
|
if (addExecPath) applyExecPath(result, pathParts, execPath, cwdPath);
|
|
1274
|
-
return pathOption === "" || pathOption === path
|
|
1266
|
+
return pathOption === "" || pathOption === path.delimiter ? `${result.join(path.delimiter)}${pathOption}` : [...result, pathOption].join(path.delimiter);
|
|
1275
1267
|
};
|
|
1276
1268
|
var applyPreferLocal = (result, pathParts, cwdPath) => {
|
|
1277
1269
|
for (const directory of traversePathUp(cwdPath)) {
|
|
1278
|
-
const pathPart = path
|
|
1270
|
+
const pathPart = path.join(directory, "node_modules/.bin");
|
|
1279
1271
|
if (!pathParts.includes(pathPart)) result.push(pathPart);
|
|
1280
1272
|
}
|
|
1281
1273
|
};
|
|
1282
1274
|
var applyExecPath = (result, pathParts, execPath, cwdPath) => {
|
|
1283
|
-
const pathPart = path
|
|
1275
|
+
const pathPart = path.resolve(cwdPath, toPath(execPath), "..");
|
|
1284
1276
|
if (!pathParts.includes(pathPart)) result.push(pathPart);
|
|
1285
1277
|
};
|
|
1286
1278
|
var npmRunPathEnv = ({ env = process$1.env, ...options } = {}) => {
|
|
@@ -2290,7 +2282,7 @@ var mapNode = ({ options }) => {
|
|
|
2290
2282
|
var handleNodeOption = (file, commandArguments, { node: shouldHandleNode = false, nodePath = execPath, nodeOptions = execArgv.filter((nodeOption) => !nodeOption.startsWith("--inspect")), cwd, execPath: formerNodePath, ...options }) => {
|
|
2291
2283
|
if (formerNodePath !== void 0) throw new TypeError("The \"execPath\" option has been removed. Please use the \"nodePath\" option instead.");
|
|
2292
2284
|
const normalizedNodePath = safeNormalizeFileUrl(nodePath, "The \"nodePath\" option");
|
|
2293
|
-
const resolvedNodePath = path
|
|
2285
|
+
const resolvedNodePath = path.resolve(cwd, normalizedNodePath);
|
|
2294
2286
|
const newOptions = {
|
|
2295
2287
|
...options,
|
|
2296
2288
|
nodePath: resolvedNodePath,
|
|
@@ -2302,7 +2294,7 @@ var handleNodeOption = (file, commandArguments, { node: shouldHandleNode = false
|
|
|
2302
2294
|
commandArguments,
|
|
2303
2295
|
newOptions
|
|
2304
2296
|
];
|
|
2305
|
-
if (path
|
|
2297
|
+
if (path.basename(file, ".exe") === "node") throw new TypeError("When the \"node\" option is true, the first argument does not need to be \"node\".");
|
|
2306
2298
|
return [
|
|
2307
2299
|
resolvedNodePath,
|
|
2308
2300
|
[
|
|
@@ -2386,7 +2378,7 @@ var serializeEncoding = (encoding) => typeof encoding === "string" ? `"${encodin
|
|
|
2386
2378
|
//#region ../../node_modules/.pnpm/execa@9.6.1/node_modules/execa/lib/arguments/cwd.js
|
|
2387
2379
|
var normalizeCwd = (cwd = getDefaultCwd()) => {
|
|
2388
2380
|
const cwdString = safeNormalizeFileUrl(cwd, "The \"cwd\" option");
|
|
2389
|
-
return path
|
|
2381
|
+
return path.resolve(cwdString);
|
|
2390
2382
|
};
|
|
2391
2383
|
var getDefaultCwd = () => {
|
|
2392
2384
|
try {
|
|
@@ -2424,7 +2416,7 @@ var normalizeOptions = (filePath, rawArguments, rawOptions) => {
|
|
|
2424
2416
|
options.killSignal = normalizeKillSignal(options.killSignal);
|
|
2425
2417
|
options.forceKillAfterDelay = normalizeForceKillAfterDelay(options.forceKillAfterDelay);
|
|
2426
2418
|
options.lines = options.lines.map((lines, fdNumber) => lines && !BINARY_ENCODINGS.has(options.encoding) && options.buffer[fdNumber]);
|
|
2427
|
-
if (process$1.platform === "win32" && path
|
|
2419
|
+
if (process$1.platform === "win32" && path.basename(file, ".exe") === "cmd") commandArguments.unshift("/q");
|
|
2428
2420
|
return {
|
|
2429
2421
|
file,
|
|
2430
2422
|
commandArguments,
|
|
@@ -6252,1231 +6244,6 @@ createExeca(mapNode);
|
|
|
6252
6244
|
createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
|
|
6253
6245
|
var { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcExport();
|
|
6254
6246
|
//#endregion
|
|
6255
|
-
//#region src/schema/destructive-sql.ts
|
|
6256
|
-
var import___vite_browser_external = /* @__PURE__ */ __toESM(require___vite_browser_external(), 1);
|
|
6257
|
-
/**
|
|
6258
|
-
* Pure helpers that classify an Atlas declarative-apply plan as destructive
|
|
6259
|
-
* or not, and decide what `rebase db push` should do about it.
|
|
6260
|
-
*
|
|
6261
|
-
* `db push` runs `atlas schema apply` to make the live database match the
|
|
6262
|
-
* generated `schema.sql`. Removing a collection field compiles to
|
|
6263
|
-
* `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
|
|
6264
|
-
* We first run the apply with `--dry-run` to obtain the planned SQL, scan it
|
|
6265
|
-
* here, and refuse to auto-approve anything destructive without an explicit
|
|
6266
|
-
* opt-in.
|
|
6267
|
-
*
|
|
6268
|
-
* A type change is a destructive plan that names no DROP. `ALTER COLUMN
|
|
6269
|
-
* "price" TYPE integer` rounds every 19.99 to 20, and `TYPE date` throws away
|
|
6270
|
-
* every timestamp's time of day, in a statement that reads like a routine
|
|
6271
|
-
* alter. So a type change is gated too, unless it is a widening every value
|
|
6272
|
-
* survives unchanged — which needs the column's current type, read from the
|
|
6273
|
-
* catalogue by the caller.
|
|
6274
|
-
*
|
|
6275
|
-
* A primary key change names no DROP of data either: `DROP CONSTRAINT
|
|
6276
|
-
* "<table>_pkey", ADD PRIMARY KEY ("id", "locale")` keeps every value and
|
|
6277
|
-
* gives every row a new address. It is gated like a drop.
|
|
6278
|
-
*
|
|
6279
|
-
* Everything in this file is side-effect free so it can be unit-tested
|
|
6280
|
-
* without Atlas or a database.
|
|
6281
|
-
*/
|
|
6282
|
-
/**
|
|
6283
|
-
* SQL fragments that destroy data or data-bearing objects. Matched
|
|
6284
|
-
* case-insensitively against each statement of the plan. `IF EXISTS` /
|
|
6285
|
-
* whitespace variations are tolerated by the regexes below.
|
|
6286
|
-
*/
|
|
6287
|
-
var DESTRUCTIVE_PATTERNS = [
|
|
6288
|
-
{
|
|
6289
|
-
label: "DROP TABLE",
|
|
6290
|
-
re: /\bDROP\s+TABLE\b/i
|
|
6291
|
-
},
|
|
6292
|
-
{
|
|
6293
|
-
label: "DROP COLUMN",
|
|
6294
|
-
re: /\bDROP\s+COLUMN\b/i
|
|
6295
|
-
},
|
|
6296
|
-
{
|
|
6297
|
-
label: "DROP SCHEMA",
|
|
6298
|
-
re: /\bDROP\s+SCHEMA\b/i
|
|
6299
|
-
},
|
|
6300
|
-
{
|
|
6301
|
-
label: "DROP VIEW",
|
|
6302
|
-
re: /\bDROP\s+(MATERIALIZED\s+)?VIEW\b/i
|
|
6303
|
-
},
|
|
6304
|
-
{
|
|
6305
|
-
label: "DROP TYPE",
|
|
6306
|
-
re: /\bDROP\s+TYPE\b/i
|
|
6307
|
-
},
|
|
6308
|
-
{
|
|
6309
|
-
label: "TRUNCATE",
|
|
6310
|
-
re: /\bTRUNCATE\b/i
|
|
6311
|
-
}
|
|
6312
|
-
];
|
|
6313
|
-
/**
|
|
6314
|
-
* Split a SQL script into individual statements, dropping blank lines and
|
|
6315
|
-
* `--` comment lines. Deliberately simple: Atlas emits one plain statement
|
|
6316
|
-
* per `;`, without string literals that contain semicolons in a schema DDL
|
|
6317
|
-
* plan, so a naive split is safe and keeps this dependency-free.
|
|
6318
|
-
*/
|
|
6319
|
-
function splitSqlStatements(sql) {
|
|
6320
|
-
return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6321
|
-
}
|
|
6322
|
-
/** `->`, the marker Atlas prints in front of the first line of every statement. */
|
|
6323
|
-
var PLAN_MARKER_RE = /^\s*->\s?/;
|
|
6324
|
-
/** `-------`, both the rule between sections and the one above a summary. */
|
|
6325
|
-
var PLAN_RULE_RE = /^\s*-{5,}\s*$/;
|
|
6326
|
-
/** `Planning migration statements (3 in total):` — and the count it announces. */
|
|
6327
|
-
var PLAN_HEADING_RE = /^Planning migration statements \((\d+) in total\):/m;
|
|
6328
|
-
/** What Atlas prints, and nothing else, when there is nothing to plan. */
|
|
6329
|
-
var PLAN_SYNCED_RE = /^\s*Schema is synced, no changes to be made\s*$/;
|
|
6330
|
-
/**
|
|
6331
|
-
* Read the statements out of whatever Atlas printed.
|
|
6332
|
-
*
|
|
6333
|
-
* `atlas schema apply --dry-run` does not print SQL. It prints a report, and
|
|
6334
|
-
* the same statements appear in it twice:
|
|
6335
|
-
*
|
|
6336
|
-
* Planning migration statements (2 in total):
|
|
6337
|
-
*
|
|
6338
|
-
* -- modify "people" table:
|
|
6339
|
-
* -> ALTER TABLE "public"."people" ALTER COLUMN "rating" TYPE numeric(4,1);
|
|
6340
|
-
* -- create "notes" table:
|
|
6341
|
-
* -> CREATE TABLE "public"."notes" (
|
|
6342
|
-
* "body" text NULL DEFAULT 'a;b',
|
|
6343
|
-
* …
|
|
6344
|
-
* );
|
|
6345
|
-
*
|
|
6346
|
-
* -------------------------------------------
|
|
6347
|
-
*
|
|
6348
|
-
* Analyzing planned statements (2 in total):
|
|
6349
|
-
* …
|
|
6350
|
-
* Running dry-run migration (2 statements in total):
|
|
6351
|
-
* -- modify "people" table
|
|
6352
|
-
* -> ALTER TABLE …; ← the same statement, again
|
|
6353
|
-
* -- ok (19µs)
|
|
6354
|
-
*
|
|
6355
|
-
* Splitting that on `;` is how the push gate went blind: the heading has no
|
|
6356
|
-
* `;` and does not start with `--`, so it became the first line of the first
|
|
6357
|
-
* statement, and an `ALTER TABLE` parse anchored at the start of a statement
|
|
6358
|
-
* read nothing — every narrowing type change applied without a prompt. Reading
|
|
6359
|
-
* both sections was a second bug, counting every destructive statement twice.
|
|
6360
|
-
*
|
|
6361
|
-
* So the report is read as a report: a statement is a `->` line and the lines
|
|
6362
|
-
* indented under it, up to the next `--` heading, `->`, blank line or rule;
|
|
6363
|
-
* only the first section that holds a `->` is read; and `;` plays no part,
|
|
6364
|
-
* because a default like `'a;b'` contains one.
|
|
6365
|
-
*
|
|
6366
|
-
* Plain SQL — a migration file, or an Atlas that printed SQL — has no `->`
|
|
6367
|
-
* and is split the old way.
|
|
6368
|
-
*/
|
|
6369
|
-
function readPlan(plan) {
|
|
6370
|
-
if (PLAN_SYNCED_RE.test(plan)) return { statements: [] };
|
|
6371
|
-
const lines = plan.split(/\r?\n/);
|
|
6372
|
-
if (!lines.some((line) => PLAN_MARKER_RE.test(line))) return { statements: splitSqlStatements(plan) };
|
|
6373
|
-
const heading = PLAN_HEADING_RE.exec(plan);
|
|
6374
|
-
const statements = [];
|
|
6375
|
-
let current = null;
|
|
6376
|
-
const flush = () => {
|
|
6377
|
-
if (current) statements.push(current.join("\n").trim().replace(/;\s*$/, "").trimEnd());
|
|
6378
|
-
current = null;
|
|
6379
|
-
};
|
|
6380
|
-
for (const line of lines) {
|
|
6381
|
-
if (PLAN_MARKER_RE.test(line)) {
|
|
6382
|
-
flush();
|
|
6383
|
-
current = [line.replace(PLAN_MARKER_RE, "")];
|
|
6384
|
-
continue;
|
|
6385
|
-
}
|
|
6386
|
-
if (PLAN_RULE_RE.test(line) || /^\S/.test(line) && !line.startsWith("--")) {
|
|
6387
|
-
flush();
|
|
6388
|
-
if (statements.length > 0) break;
|
|
6389
|
-
continue;
|
|
6390
|
-
}
|
|
6391
|
-
if (!current) continue;
|
|
6392
|
-
if (line.trim() === "" || line.trim().startsWith("--")) {
|
|
6393
|
-
flush();
|
|
6394
|
-
continue;
|
|
6395
|
-
}
|
|
6396
|
-
current.push(line);
|
|
6397
|
-
}
|
|
6398
|
-
flush();
|
|
6399
|
-
return {
|
|
6400
|
-
statements,
|
|
6401
|
-
announced: heading ? Number(heading[1]) : void 0
|
|
6402
|
-
};
|
|
6403
|
-
}
|
|
6404
|
-
/**
|
|
6405
|
-
* The statements of an Atlas plan, each exactly once, without Atlas's
|
|
6406
|
-
* rendering and without the trailing `;`. See {@link readPlan} for the shape
|
|
6407
|
-
* it reads; `test/fixtures/atlas-dry-run/` holds real captures of it.
|
|
6408
|
-
*/
|
|
6409
|
-
function extractPlanStatements(plan) {
|
|
6410
|
-
return readPlan(plan).statements;
|
|
6411
|
-
}
|
|
6412
|
-
/**
|
|
6413
|
-
* Every column of every ordinary table outside the system schemas, with its
|
|
6414
|
-
* type. What {@link detectDestructiveStatements} compares a planned type
|
|
6415
|
-
* change against; `format_type` because it carries the modifier (`(255)`,
|
|
6416
|
-
* `(10,2)`) that decides whether a change widens or narrows.
|
|
6417
|
-
*/
|
|
6418
|
-
var COLUMN_TYPES_SQL = `
|
|
6419
|
-
SELECT n.nspname AS "schema",
|
|
6420
|
-
c.relname AS "table",
|
|
6421
|
-
a.attname AS "column",
|
|
6422
|
-
format_type(a.atttypid, a.atttypmod) AS "type"
|
|
6423
|
-
FROM pg_attribute a
|
|
6424
|
-
JOIN pg_class c ON c.oid = a.attrelid
|
|
6425
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
6426
|
-
WHERE c.relkind IN ('r', 'p')
|
|
6427
|
-
AND a.attnum > 0
|
|
6428
|
-
AND NOT a.attisdropped
|
|
6429
|
-
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
6430
|
-
AND n.nspname NOT LIKE 'pg\\_%'
|
|
6431
|
-
`;
|
|
6432
|
-
/**
|
|
6433
|
-
* Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
|
|
6434
|
-
* the statements that would destroy data. An empty array means the plan is
|
|
6435
|
-
* safe to auto-approve.
|
|
6436
|
-
*
|
|
6437
|
-
* `columnTypes` is what the database holds now. A type change to a column it
|
|
6438
|
-
* does not list is flagged: not knowing what a column is means not knowing
|
|
6439
|
-
* whether the change keeps its values.
|
|
6440
|
-
*/
|
|
6441
|
-
function detectDestructiveStatements(planSql, columnTypes = []) {
|
|
6442
|
-
const found = [];
|
|
6443
|
-
const { statements, announced } = readPlan(planSql);
|
|
6444
|
-
if (announced !== void 0 && announced !== statements.length) found.push({
|
|
6445
|
-
statement: `Planning migration statements (${announced} in total)`,
|
|
6446
|
-
kind: "UNREADABLE PLAN",
|
|
6447
|
-
detail: `Atlas announced ${announced} statement(s) and ${statements.length} could be read — read the plan yourself before allowing it`
|
|
6448
|
-
});
|
|
6449
|
-
for (const statement of statements) {
|
|
6450
|
-
const dropped = DESTRUCTIVE_PATTERNS.find(({ re }) => re.test(statement));
|
|
6451
|
-
const rekeyed = describePrimaryKeyChange(statement);
|
|
6452
|
-
const lossy = parseColumnTypeChanges(statement).map((change) => ({
|
|
6453
|
-
change,
|
|
6454
|
-
from: currentTypeOf(change, columnTypes)
|
|
6455
|
-
})).filter(({ change, from }) => from === void 0 || !isLosslessTypeChange(from, change.to)).map(({ change, from }) => `"${change.column}" ${from ?? "(current type unknown)"} → ${change.to}`).join("; ");
|
|
6456
|
-
const kind = dropped?.label ?? (rekeyed ? "PRIMARY KEY CHANGE" : lossy ? "ALTER COLUMN TYPE" : void 0);
|
|
6457
|
-
if (!kind) continue;
|
|
6458
|
-
const detail = [rekeyed, lossy].filter(Boolean).join("; ");
|
|
6459
|
-
found.push(detail ? {
|
|
6460
|
-
statement,
|
|
6461
|
-
kind,
|
|
6462
|
-
detail
|
|
6463
|
-
} : {
|
|
6464
|
-
statement,
|
|
6465
|
-
kind
|
|
6466
|
-
});
|
|
6467
|
-
}
|
|
6468
|
-
return found;
|
|
6469
|
-
}
|
|
6470
|
-
var IDENT = String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)`;
|
|
6471
|
-
var ALTER_TABLE_RE = new RegExp(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?(${IDENT})(?:\.(${IDENT}))?\s+([\s\S]*)$`, "i");
|
|
6472
|
-
var TYPE_CLAUSE_RE = new RegExp(String.raw`^ALTER\s+(?:COLUMN\s+)?(${IDENT})\s+(?:SET\s+DATA\s+)?TYPE\s+([\s\S]+)$`, "i");
|
|
6473
|
-
var unquote = (ident) => ident.startsWith("\"") ? ident.slice(1, -1).replace(/""/g, "\"") : ident;
|
|
6474
|
-
/**
|
|
6475
|
-
* Split an `ALTER TABLE`'s clause list on its top-level commas — the ones
|
|
6476
|
-
* outside parentheses and quotes, so `TYPE numeric(10, 2)` stays one clause.
|
|
6477
|
-
*/
|
|
6478
|
-
function splitClauses(body) {
|
|
6479
|
-
const clauses = [];
|
|
6480
|
-
let depth = 0;
|
|
6481
|
-
let quote;
|
|
6482
|
-
let start = 0;
|
|
6483
|
-
for (let i = 0; i < body.length; i++) {
|
|
6484
|
-
const ch = body[i];
|
|
6485
|
-
if (quote) {
|
|
6486
|
-
if (ch === quote) quote = void 0;
|
|
6487
|
-
} else if (ch === "\"" || ch === "'") quote = ch;
|
|
6488
|
-
else if (ch === "(") depth++;
|
|
6489
|
-
else if (ch === ")") depth--;
|
|
6490
|
-
else if (ch === "," && depth === 0) {
|
|
6491
|
-
clauses.push(body.slice(start, i).trim());
|
|
6492
|
-
start = i + 1;
|
|
6493
|
-
}
|
|
6494
|
-
}
|
|
6495
|
-
clauses.push(body.slice(start).trim());
|
|
6496
|
-
return clauses.filter((clause) => clause.length > 0);
|
|
6497
|
-
}
|
|
6498
|
-
/**
|
|
6499
|
-
* The statement from its `ALTER TABLE` on, wherever on a line that starts.
|
|
6500
|
-
*
|
|
6501
|
-
* {@link readPlan} hands over clean statements. This is the second line of
|
|
6502
|
-
* defence for text it could not clean — a rendering it does not know yet —
|
|
6503
|
-
* where a line of prose above the statement must not hide the statement, which
|
|
6504
|
-
* is exactly how an anchored match once read Atlas's whole plan as safe.
|
|
6505
|
-
*/
|
|
6506
|
-
function fromAlterTable(statement) {
|
|
6507
|
-
const start = /(?:^|\n)[ \t]*(?:->[ \t]*)?(ALTER\s+TABLE\b)/i.exec(statement);
|
|
6508
|
-
return start ? statement.slice(start.index + start[0].length - start[1].length).trim() : statement.trim();
|
|
6509
|
-
}
|
|
6510
|
-
/**
|
|
6511
|
-
* Every `ALTER COLUMN … [SET DATA] TYPE` in one statement. Atlas prints its
|
|
6512
|
-
* dry-run statements under a `-> ` marker and puts several clauses in one
|
|
6513
|
-
* `ALTER TABLE`, so both are handled.
|
|
6514
|
-
*/
|
|
6515
|
-
function parseColumnTypeChanges(statement) {
|
|
6516
|
-
const table = ALTER_TABLE_RE.exec(fromAlterTable(statement));
|
|
6517
|
-
if (!table) return [];
|
|
6518
|
-
const [schema, name] = table[2] ? [unquote(table[1]), unquote(table[2])] : [void 0, unquote(table[1])];
|
|
6519
|
-
const changes = [];
|
|
6520
|
-
for (const clause of splitClauses(table[3])) {
|
|
6521
|
-
const match = TYPE_CLAUSE_RE.exec(clause);
|
|
6522
|
-
if (!match) continue;
|
|
6523
|
-
const to = match[2].split(/\s+(?:USING|COLLATE)\s/i)[0].trim();
|
|
6524
|
-
changes.push({
|
|
6525
|
-
schema,
|
|
6526
|
-
table: name,
|
|
6527
|
-
column: unquote(match[1]),
|
|
6528
|
-
to
|
|
6529
|
-
});
|
|
6530
|
-
}
|
|
6531
|
-
return changes;
|
|
6532
|
-
}
|
|
6533
|
-
/** `ADD [CONSTRAINT "x"] PRIMARY KEY ("a", "b")`, one clause of an `ALTER TABLE`. */
|
|
6534
|
-
var ADD_PRIMARY_KEY_RE = new RegExp(String.raw`^ADD\s+(?:CONSTRAINT\s+${IDENT}\s+)?PRIMARY\s+KEY\b\s*(?:\(([^)]*)\))?`, "i");
|
|
6535
|
-
/** `DROP CONSTRAINT [IF EXISTS] "x"`, one clause of an `ALTER TABLE`. */
|
|
6536
|
-
var DROP_CONSTRAINT_RE = new RegExp(String.raw`^DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?(${IDENT})`, "i");
|
|
6537
|
-
/** What a row loses when the columns that address it change. */
|
|
6538
|
-
var ROW_ADDRESS_LOST = "every row's id changes, so anything that refers to a row by its id — a URL, a foreign key, an id stored elsewhere — stops finding it";
|
|
6539
|
-
/**
|
|
6540
|
-
* What an `ALTER TABLE` does to the table's primary key, in a sentence — or
|
|
6541
|
-
* `undefined` when it leaves the key alone.
|
|
6542
|
-
*
|
|
6543
|
-
* The key is a row's address: Rebase reads, links and routes every row by it.
|
|
6544
|
-
* Atlas plans a key change as `DROP CONSTRAINT "<table>_pkey", ADD PRIMARY KEY
|
|
6545
|
-
* (…)` — one routine-looking statement that names no DROP of data — so moving
|
|
6546
|
-
* a collection from `(id)` to `(id, locale)` re-keyed a populated table with
|
|
6547
|
-
* no prompt. A `CREATE TABLE`'s own `PRIMARY KEY` is not a change and is never
|
|
6548
|
-
* read here: only an `ALTER TABLE`'s clauses are. A dropped constraint counts
|
|
6549
|
-
* as the key when it carries Postgres's default name for one, `<table>_pkey`;
|
|
6550
|
-
* a key under another name is caught by the `ADD PRIMARY KEY` that replaces it.
|
|
6551
|
-
*/
|
|
6552
|
-
function describePrimaryKeyChange(statement) {
|
|
6553
|
-
const alter = ALTER_TABLE_RE.exec(fromAlterTable(statement));
|
|
6554
|
-
if (!alter) return void 0;
|
|
6555
|
-
const table = alter[2] ? `${alter[1]}.${alter[2]}` : alter[1];
|
|
6556
|
-
let added;
|
|
6557
|
-
let dropped;
|
|
6558
|
-
for (const clause of splitClauses(alter[3])) {
|
|
6559
|
-
const add = ADD_PRIMARY_KEY_RE.exec(clause);
|
|
6560
|
-
if (add) {
|
|
6561
|
-
added = add[1] === void 0 ? "a new key" : `(${add[1].split(",").map((c) => c.trim()).join(", ")})`;
|
|
6562
|
-
continue;
|
|
6563
|
-
}
|
|
6564
|
-
const drop = DROP_CONSTRAINT_RE.exec(clause);
|
|
6565
|
-
if (drop && unquote(drop[1]).endsWith("_pkey")) dropped = drop[1];
|
|
6566
|
-
}
|
|
6567
|
-
if (added && dropped) return `${table} is re-keyed on ${added}: ${ROW_ADDRESS_LOST}`;
|
|
6568
|
-
if (added) return `${table} gets the primary key ${added}: rows are addressed by it from now on, and the push fails if two rows share a value`;
|
|
6569
|
-
if (dropped) return `${table} loses its primary key ${dropped}: ${ROW_ADDRESS_LOST}`;
|
|
6570
|
-
}
|
|
6571
|
-
/**
|
|
6572
|
-
* The column's current type. A table named without its schema matches in any
|
|
6573
|
-
* schema, but only when every match agrees — two same-named tables with
|
|
6574
|
-
* different types leave the answer unknown.
|
|
6575
|
-
*/
|
|
6576
|
-
function currentTypeOf(change, columnTypes) {
|
|
6577
|
-
const types = new Set(columnTypes.filter((col) => col.table === change.table && col.column === change.column && (change.schema === void 0 || col.schema === change.schema)).map((col) => col.type));
|
|
6578
|
-
return types.size === 1 ? [...types][0] : void 0;
|
|
6579
|
-
}
|
|
6580
|
-
/** Spellings of one type, mapped to one name. Anything absent compares verbatim. */
|
|
6581
|
-
var TYPE_ALIASES = {
|
|
6582
|
-
"int": "integer",
|
|
6583
|
-
"int4": "integer",
|
|
6584
|
-
"int2": "smallint",
|
|
6585
|
-
"int8": "bigint",
|
|
6586
|
-
"character varying": "varchar",
|
|
6587
|
-
"decimal": "numeric",
|
|
6588
|
-
"timestamp without time zone": "timestamp",
|
|
6589
|
-
"timestamp with time zone": "timestamptz",
|
|
6590
|
-
"time without time zone": "time",
|
|
6591
|
-
"time with time zone": "timetz",
|
|
6592
|
-
"bool": "boolean",
|
|
6593
|
-
"float4": "real",
|
|
6594
|
-
"float8": "double precision",
|
|
6595
|
-
"character": "bpchar",
|
|
6596
|
-
"char": "bpchar"
|
|
6597
|
-
};
|
|
6598
|
-
/** Decimal digits each integer type holds, which is what a `numeric` must hold too. */
|
|
6599
|
-
var INTEGER_DIGITS = {
|
|
6600
|
-
smallint: 5,
|
|
6601
|
-
integer: 10,
|
|
6602
|
-
bigint: 19
|
|
6603
|
-
};
|
|
6604
|
-
/** Types whose modifier is a fractional-seconds precision, 6 when absent. */
|
|
6605
|
-
var SECONDS_PRECISION = /* @__PURE__ */ new Set([
|
|
6606
|
-
"timestamp",
|
|
6607
|
-
"timestamptz",
|
|
6608
|
-
"time",
|
|
6609
|
-
"timetz"
|
|
6610
|
-
]);
|
|
6611
|
-
function parseType(raw) {
|
|
6612
|
-
let text = raw.trim().replace(/\s+/g, " ");
|
|
6613
|
-
let dims = 0;
|
|
6614
|
-
for (let suffix = /\s*\[\d*\]$/.exec(text); suffix; suffix = /\s*\[\d*\]$/.exec(text)) {
|
|
6615
|
-
dims++;
|
|
6616
|
-
text = text.slice(0, suffix.index);
|
|
6617
|
-
}
|
|
6618
|
-
if (text.includes("\"")) return {
|
|
6619
|
-
base: text,
|
|
6620
|
-
dims
|
|
6621
|
-
};
|
|
6622
|
-
text = text.toLowerCase().replace(/^pg_catalog\./, "");
|
|
6623
|
-
let args;
|
|
6624
|
-
const modifier = /\(([^)]*)\)/.exec(text);
|
|
6625
|
-
if (modifier) {
|
|
6626
|
-
args = modifier[1].split(",").map((part) => Number(part.trim()));
|
|
6627
|
-
text = `${text.slice(0, modifier.index)} ${text.slice(modifier.index + modifier[0].length)}`.replace(/\s+/g, " ").trim();
|
|
6628
|
-
}
|
|
6629
|
-
return {
|
|
6630
|
-
base: TYPE_ALIASES[text] ?? text,
|
|
6631
|
-
args,
|
|
6632
|
-
dims
|
|
6633
|
-
};
|
|
6634
|
-
}
|
|
6635
|
-
/** Does moving from one modifier to another, on the same type, keep every value? */
|
|
6636
|
-
function modifierWidens(base, from, to) {
|
|
6637
|
-
if (JSON.stringify(from) === JSON.stringify(to)) return true;
|
|
6638
|
-
if (base === "varchar") return to === void 0 || from !== void 0 && to[0] >= from[0];
|
|
6639
|
-
if (base === "numeric") {
|
|
6640
|
-
if (to === void 0) return true;
|
|
6641
|
-
if (from === void 0) return false;
|
|
6642
|
-
const [p1, s1 = 0] = from;
|
|
6643
|
-
const [p2, s2 = 0] = to;
|
|
6644
|
-
return s2 >= s1 && p2 - s2 >= p1 - s1;
|
|
6645
|
-
}
|
|
6646
|
-
if (SECONDS_PRECISION.has(base)) return (to?.[0] ?? 6) >= (from?.[0] ?? 6);
|
|
6647
|
-
return false;
|
|
6648
|
-
}
|
|
6649
|
-
/**
|
|
6650
|
-
* Is `ALTER COLUMN … TYPE to` a widening, one that keeps every value exactly
|
|
6651
|
-
* as it was?
|
|
6652
|
-
*
|
|
6653
|
-
* Deliberately short. Everything not listed needs a confirmation, including
|
|
6654
|
-
* changes that are usually harmless: `timestamp` → `timestamptz` reads each
|
|
6655
|
-
* value in the session's time zone, `json` → `jsonb` drops duplicate keys,
|
|
6656
|
-
* `real` → `double precision` prints 0.1 as 0.100000001490116, and `integer` →
|
|
6657
|
-
* `text` keeps the digits but changes what the column is.
|
|
6658
|
-
*
|
|
6659
|
-
* - the same type, with a modifier at least as wide (`varchar(100)` →
|
|
6660
|
-
* `varchar(255)`, `numeric(10,2)` → `numeric(12,2)` or unbounded,
|
|
6661
|
-
* `timestamptz(3)` → `timestamptz`);
|
|
6662
|
-
* - `smallint` → `integer` → `bigint`, and any of them to a `numeric` with
|
|
6663
|
-
* room for all their digits;
|
|
6664
|
-
* - `varchar(n)` → `text`, and `text` → an unbounded `varchar`.
|
|
6665
|
-
*
|
|
6666
|
-
* Arrays follow their element type, and never gain or lose a dimension.
|
|
6667
|
-
*/
|
|
6668
|
-
function isLosslessTypeChange(from, to) {
|
|
6669
|
-
const a = parseType(from);
|
|
6670
|
-
const b = parseType(to);
|
|
6671
|
-
if (a.dims !== b.dims) return false;
|
|
6672
|
-
if (a.args?.some(Number.isNaN) || b.args?.some(Number.isNaN)) return false;
|
|
6673
|
-
if (a.base === b.base) return modifierWidens(a.base, a.args, b.args);
|
|
6674
|
-
const fromDigits = INTEGER_DIGITS[a.base];
|
|
6675
|
-
if (fromDigits !== void 0) {
|
|
6676
|
-
const toDigits = INTEGER_DIGITS[b.base];
|
|
6677
|
-
if (toDigits !== void 0) return toDigits >= fromDigits;
|
|
6678
|
-
if (b.base === "numeric") return b.args === void 0 || b.args[0] - (b.args[1] ?? 0) >= fromDigits;
|
|
6679
|
-
return false;
|
|
6680
|
-
}
|
|
6681
|
-
if (a.base === "varchar" && b.base === "text") return true;
|
|
6682
|
-
if (a.base === "text" && b.base === "varchar") return b.args === void 0;
|
|
6683
|
-
return false;
|
|
6684
|
-
}
|
|
6685
|
-
/**
|
|
6686
|
-
* Decide how `db push` should proceed given the plan's destructiveness and
|
|
6687
|
-
* the invocation context.
|
|
6688
|
-
*
|
|
6689
|
-
* - No destructive statements → `apply` (safe to auto-approve).
|
|
6690
|
-
* - Destructive + `--allow-destructive` → `apply` (operator opted in).
|
|
6691
|
-
* - Destructive + interactive TTY → `confirm` (prompt before applying).
|
|
6692
|
-
* - Destructive + non-interactive → `refuse` (never silently drop data in
|
|
6693
|
-
* CI / scripts / agents).
|
|
6694
|
-
*/
|
|
6695
|
-
function decidePushSafety(opts) {
|
|
6696
|
-
if (opts.destructiveCount === 0) return "apply";
|
|
6697
|
-
if (opts.allowDestructive) return "apply";
|
|
6698
|
-
return opts.interactive ? "confirm" : "refuse";
|
|
6699
|
-
}
|
|
6700
|
-
//#endregion
|
|
6701
|
-
//#region src/module-dir.ts
|
|
6702
|
-
var moduleDir = path.dirname(fileURLToPath$1(import.meta.url));
|
|
6703
|
-
//#endregion
|
|
6704
|
-
//#region src/generated-sql.ts
|
|
6705
|
-
/**
|
|
6706
|
-
* Where `db push` and `db generate` put the SQL they render from the collections.
|
|
6707
|
-
*
|
|
6708
|
-
* Five files — `schema.sql` (Atlas's desired state), `policies.sql`,
|
|
6709
|
-
* `search.sql`, `vector.sql` and `triggers.sql` — and every one of them is
|
|
6710
|
-
* scratch. Both commands regenerate all five before reading any of them, and
|
|
6711
|
-
* nothing else reads them at all. They used to be written into `drizzle/`, beside
|
|
6712
|
-
* the migrations, and committed: a copy nobody read, which every collection
|
|
6713
|
-
* change rewrote in the diff and every two branches touching collections
|
|
6714
|
-
* conflicted on.
|
|
6715
|
-
*
|
|
6716
|
-
* Relative to the directory the command runs in (the backend), like the
|
|
6717
|
-
* migrations are.
|
|
6718
|
-
*/
|
|
6719
|
-
/** The directory, relative to the working directory. POSIX, because it is also a URL path. */
|
|
6720
|
-
var GENERATED_SQL_DIR = ".rebase/sql";
|
|
6721
|
-
/**
|
|
6722
|
-
* `schema.sql` as Atlas takes it. Relative on purpose, like the
|
|
6723
|
-
* `file://drizzle/migrations` beside it: Atlas joins the URL's host and path, so
|
|
6724
|
-
* `.rebase` is read as the first path segment, measured against the pinned 1.2.3.
|
|
6725
|
-
*/
|
|
6726
|
-
var DESIRED_STATE_URL = `file://${GENERATED_SQL_DIR}/schema.sql`;
|
|
6727
|
-
/** The files the DDL generator writes, all of them derived from the collections. */
|
|
6728
|
-
var GENERATED_SQL_FILES = [
|
|
6729
|
-
"schema.sql",
|
|
6730
|
-
"policies.sql",
|
|
6731
|
-
"search.sql",
|
|
6732
|
-
"vector.sql",
|
|
6733
|
-
"triggers.sql"
|
|
6734
|
-
];
|
|
6735
|
-
/** The absolute directory, for the working directory the command runs in. */
|
|
6736
|
-
function generatedSqlDir(cwd = process.cwd()) {
|
|
6737
|
-
return path.resolve(cwd, GENERATED_SQL_DIR);
|
|
6738
|
-
}
|
|
6739
|
-
/**
|
|
6740
|
-
* The first line of every file the generator writes, in every release that
|
|
6741
|
-
* wrote them into `drizzle/`. Matching it is what makes removing the old copy
|
|
6742
|
-
* safe: a file there under one of these names that does not open this way was
|
|
6743
|
-
* written by a person, and is left alone.
|
|
6744
|
-
*/
|
|
6745
|
-
var GENERATED_HEADER = /^-- This file (is auto-generated by the Rebase DDL generator|contains RLS policies generated by Rebase)\./;
|
|
6746
|
-
/**
|
|
6747
|
-
* Get the directory ready to be written, and retire the copies an older release
|
|
6748
|
-
* committed.
|
|
6749
|
-
*
|
|
6750
|
-
* The directory ignores itself, rather than relying on the project's
|
|
6751
|
-
* `.gitignore`: a scaffold ignores `.rebase/`, but a project created before it
|
|
6752
|
-
* did would otherwise start committing these files from their new home.
|
|
6753
|
-
*
|
|
6754
|
-
* The old copies in `drizzle/` are deleted, not left behind. Nothing writes them
|
|
6755
|
-
* any more, so from the first push after an upgrade they only fall further
|
|
6756
|
-
* behind — and a stale `policies.sql` in the repository is exactly what someone
|
|
6757
|
-
* reviewing the project's RLS would read.
|
|
6758
|
-
*
|
|
6759
|
-
* @returns the old files it removed, relative to the working directory.
|
|
6760
|
-
*/
|
|
6761
|
-
function prepareGeneratedSqlDir(cwd = process.cwd()) {
|
|
6762
|
-
const dir = generatedSqlDir(cwd);
|
|
6763
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
6764
|
-
const ignore = path.join(dir, ".gitignore");
|
|
6765
|
-
if (!fs.existsSync(ignore)) fs.writeFileSync(ignore, "# Generated by `rebase db push` / `rebase db generate` from the collections.\n*\n");
|
|
6766
|
-
const removed = [];
|
|
6767
|
-
for (const name of GENERATED_SQL_FILES) {
|
|
6768
|
-
const legacy = path.join(cwd, "drizzle", name);
|
|
6769
|
-
if (!fs.existsSync(legacy)) continue;
|
|
6770
|
-
const firstLine = fs.readFileSync(legacy, "utf-8").split("\n", 1)[0];
|
|
6771
|
-
if (!GENERATED_HEADER.test(firstLine)) continue;
|
|
6772
|
-
fs.rmSync(legacy);
|
|
6773
|
-
removed.push(path.posix.join("drizzle", name));
|
|
6774
|
-
}
|
|
6775
|
-
return removed;
|
|
6776
|
-
}
|
|
6777
|
-
//#endregion
|
|
6778
|
-
//#region src/cli-helpers.ts
|
|
6779
|
-
/**
|
|
6780
|
-
* Which package manager installed this project, read off its lockfile.
|
|
6781
|
-
*
|
|
6782
|
-
* Every remedy for a blocked build script is package-manager specific, and
|
|
6783
|
-
* naming the wrong one is the same failure as naming none: `pnpm approve-builds`
|
|
6784
|
-
* is not a thing an npm user can run, and `pnpm.onlyBuiltDependencies` is a key
|
|
6785
|
-
* npm does not read. This existed as a pnpm-only message for long enough that
|
|
6786
|
-
* npm 12 — which blocks dependency lifecycle scripts by default, the way pnpm 10
|
|
6787
|
-
* does — could ship and leave every npm reader three unfollowable commands deep.
|
|
6788
|
-
*
|
|
6789
|
-
* The lockfile, not `npm_config_user_agent`: the question is how the project's
|
|
6790
|
-
* `node_modules` was built, not which binary happens to be invoking us. Walks up
|
|
6791
|
-
* because commands run from `backend/` in a scaffolded project, and the lockfile
|
|
6792
|
-
* is at the workspace root.
|
|
6793
|
-
*/
|
|
6794
|
-
function detectProjectPackageManager(startDir = process.cwd()) {
|
|
6795
|
-
const lockfiles = [
|
|
6796
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
6797
|
-
["bun.lock", "bun"],
|
|
6798
|
-
["bun.lockb", "bun"],
|
|
6799
|
-
["yarn.lock", "yarn"],
|
|
6800
|
-
["package-lock.json", "npm"]
|
|
6801
|
-
];
|
|
6802
|
-
let dir = path.resolve(startDir);
|
|
6803
|
-
while (true) {
|
|
6804
|
-
for (const [file, manager] of lockfiles) if (fs.existsSync(path.join(dir, file))) return manager;
|
|
6805
|
-
const parent = path.dirname(dir);
|
|
6806
|
-
if (parent === dir) return "unknown";
|
|
6807
|
-
dir = parent;
|
|
6808
|
-
}
|
|
6809
|
-
}
|
|
6810
|
-
/**
|
|
6811
|
-
* How to let `@ariga/atlas` run its `preinstall`, in the reader's own package
|
|
6812
|
-
* manager.
|
|
6813
|
-
*
|
|
6814
|
-
* Returns the lines to print after "the binary is missing but the package is
|
|
6815
|
-
* installed". Kept next to {@link detectProjectPackageManager} so the two move
|
|
6816
|
-
* together, and returned rather than printed so it can be asserted on.
|
|
6817
|
-
*/
|
|
6818
|
-
function describeBuildScriptRemedy(packageName, manager = detectProjectPackageManager()) {
|
|
6819
|
-
switch (manager) {
|
|
6820
|
-
case "npm": return [
|
|
6821
|
-
` npm install-scripts approve ${packageName}`,
|
|
6822
|
-
"",
|
|
6823
|
-
" or, to record it in the project (what `rebase init` scaffolds):",
|
|
6824
|
-
"",
|
|
6825
|
-
" // package.json",
|
|
6826
|
-
` "allowScripts": { "${packageName}": true }`,
|
|
6827
|
-
"",
|
|
6828
|
-
" Then re-run `npm install`."
|
|
6829
|
-
];
|
|
6830
|
-
case "yarn": return [
|
|
6831
|
-
" // .yarnrc.yml",
|
|
6832
|
-
" enableScripts: true",
|
|
6833
|
-
"",
|
|
6834
|
-
" Then re-run `yarn install`."
|
|
6835
|
-
];
|
|
6836
|
-
case "bun": return [
|
|
6837
|
-
" // package.json",
|
|
6838
|
-
` "trustedDependencies": ["${packageName}"]`,
|
|
6839
|
-
"",
|
|
6840
|
-
" Then re-run `bun install`."
|
|
6841
|
-
];
|
|
6842
|
-
default: return [
|
|
6843
|
-
" pnpm approve-builds",
|
|
6844
|
-
"",
|
|
6845
|
-
" or, to record it in the project (what `rebase init` scaffolds):",
|
|
6846
|
-
"",
|
|
6847
|
-
" // package.json",
|
|
6848
|
-
` "pnpm": { "onlyBuiltDependencies": ["${packageName}"] }`,
|
|
6849
|
-
"",
|
|
6850
|
-
" Then re-run `pnpm install`."
|
|
6851
|
-
];
|
|
6852
|
-
}
|
|
6853
|
-
}
|
|
6854
|
-
/**
|
|
6855
|
-
* "Install again, from scratch", in the reader's own package manager.
|
|
6856
|
-
*
|
|
6857
|
-
* For the `bin-link-missing` state: the tree is half-written, and re-resolving
|
|
6858
|
-
* it is what recreates the `.bin` shim. `--force` on pnpm is what makes it
|
|
6859
|
-
* relink rather than decide the tree is already up to date.
|
|
6860
|
-
*/
|
|
6861
|
-
function describeReinstallCommand(manager = detectProjectPackageManager()) {
|
|
6862
|
-
switch (manager) {
|
|
6863
|
-
case "npm": return "rm -rf node_modules && npm install";
|
|
6864
|
-
case "yarn": return "yarn install --force";
|
|
6865
|
-
case "bun": return "rm -rf node_modules && bun install";
|
|
6866
|
-
default: return "pnpm install --force";
|
|
6867
|
-
}
|
|
6868
|
-
}
|
|
6869
|
-
/** `add <pkg>` as a dev dependency, in the reader's own package manager. */
|
|
6870
|
-
function describeDevAddCommand(packageName, manager = detectProjectPackageManager()) {
|
|
6871
|
-
switch (manager) {
|
|
6872
|
-
case "npm": return `npm install -D ${packageName}`;
|
|
6873
|
-
case "yarn": return `yarn add -D ${packageName}`;
|
|
6874
|
-
case "bun": return `bun add -d ${packageName}`;
|
|
6875
|
-
default: return `pnpm add -D ${packageName}`;
|
|
6876
|
-
}
|
|
6877
|
-
}
|
|
6878
|
-
/**
|
|
6879
|
-
* Where a package's own manifest lives, or null if it is not installed here.
|
|
6880
|
-
*
|
|
6881
|
-
* `package.json` rather than the package root: a package with an `exports` map
|
|
6882
|
-
* that omits `.` is unresolvable by name even when it is perfectly installed,
|
|
6883
|
-
* which would misreport it as absent.
|
|
6884
|
-
*/
|
|
6885
|
-
function resolvePackageManifest(packageName) {
|
|
6886
|
-
const bases = [pathToFileURL(path.join(process.cwd(), "package.json")), pathToFileURL(path.join(moduleDir, "package.json"))];
|
|
6887
|
-
for (const base of bases) try {
|
|
6888
|
-
return createRequire(base).resolve(`${packageName}/package.json`);
|
|
6889
|
-
} catch {}
|
|
6890
|
-
return null;
|
|
6891
|
-
}
|
|
6892
|
-
/**
|
|
6893
|
-
* The binaries a package's own manifest declares, as absolute paths.
|
|
6894
|
-
*
|
|
6895
|
-
* `bin` is a string for a single binary named after the package, or a map.
|
|
6896
|
-
* Both forms appear in the wild and `@ariga/atlas` uses the map.
|
|
6897
|
-
*/
|
|
6898
|
-
function declaredBinaries(manifestPath) {
|
|
6899
|
-
let manifest;
|
|
6900
|
-
try {
|
|
6901
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
6902
|
-
} catch {
|
|
6903
|
-
return [];
|
|
6904
|
-
}
|
|
6905
|
-
const dir = path.dirname(manifestPath);
|
|
6906
|
-
if (typeof manifest.bin === "string") return [path.resolve(dir, manifest.bin)];
|
|
6907
|
-
if (manifest.bin && typeof manifest.bin === "object") return Object.values(manifest.bin).filter((target) => typeof target === "string").map((target) => path.resolve(dir, target));
|
|
6908
|
-
return [];
|
|
6909
|
-
}
|
|
6910
|
-
function diagnoseMissingBin(packageName) {
|
|
6911
|
-
const manifestPath = resolvePackageManifest(packageName);
|
|
6912
|
-
if (!manifestPath) return "not-installed";
|
|
6913
|
-
const binaries = declaredBinaries(manifestPath);
|
|
6914
|
-
if (binaries.length === 0) return "build-script-blocked";
|
|
6915
|
-
return binaries.some((file) => fs.existsSync(file)) ? "bin-link-missing" : "build-script-blocked";
|
|
6916
|
-
}
|
|
6917
|
-
function resolveLocalBin(binName) {
|
|
6918
|
-
let dir = moduleDir;
|
|
6919
|
-
while (true) {
|
|
6920
|
-
const candidate = path.join(dir, "node_modules", ".bin", binName);
|
|
6921
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
6922
|
-
const parent = path.dirname(dir);
|
|
6923
|
-
if (parent === dir) break;
|
|
6924
|
-
dir = parent;
|
|
6925
|
-
}
|
|
6926
|
-
let cwd = process.cwd();
|
|
6927
|
-
while (true) {
|
|
6928
|
-
const candidate = path.join(cwd, "node_modules", ".bin", binName);
|
|
6929
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
6930
|
-
const parent = path.dirname(cwd);
|
|
6931
|
-
if (parent === cwd) break;
|
|
6932
|
-
cwd = parent;
|
|
6933
|
-
}
|
|
6934
|
-
try {
|
|
6935
|
-
const globalPath = execSync(process.platform === "win32" ? `where ${binName}` : `which ${binName}`, { encoding: "utf-8" }).trim().split("\n")[0].trim();
|
|
6936
|
-
if (globalPath && fs.existsSync(globalPath)) return globalPath;
|
|
6937
|
-
} catch {}
|
|
6938
|
-
return null;
|
|
6939
|
-
}
|
|
6940
|
-
async function getTableIncludesFromCollections(allCollections) {
|
|
6941
|
-
const { getTableName, relationalCollections, resolveJunctionSpecs } = await import("@rebasepro/common");
|
|
6942
|
-
const { isPostgresCollectionConfig } = await import("@rebasepro/types");
|
|
6943
|
-
const collections = relationalCollections(allCollections);
|
|
6944
|
-
const includes = [];
|
|
6945
|
-
for (const col of collections) {
|
|
6946
|
-
const tableName = getTableName(col);
|
|
6947
|
-
const schema = isPostgresCollectionConfig(col) && col.schema ? col.schema : "public";
|
|
6948
|
-
if (tableName) includes.push(`${schema}.${tableName}`);
|
|
6949
|
-
}
|
|
6950
|
-
for (const junction of resolveJunctionSpecs(collections).values()) includes.push(`${junction.schema}.${junction.table}`);
|
|
6951
|
-
return Array.from(new Set(includes));
|
|
6952
|
-
}
|
|
6953
|
-
async function getTableIncludes(collectionsPath) {
|
|
6954
|
-
return getTableIncludesFromCollections(await loadCollectionsForCli(collectionsPath));
|
|
6955
|
-
}
|
|
6956
|
-
/**
|
|
6957
|
-
* Load a project's collections the way the Atlas-facing commands need them.
|
|
6958
|
-
*
|
|
6959
|
-
* Deliberately forgiving — a file that fails to import is skipped rather than
|
|
6960
|
-
* fatal — because the callers use this to *narrow* what Atlas may touch, and a
|
|
6961
|
-
* hard failure here would block a push over an unrelated broken file. Callers
|
|
6962
|
-
* that cannot tolerate a partial answer (the table excludes, which fail closed)
|
|
6963
|
-
* check the result themselves.
|
|
6964
|
-
*/
|
|
6965
|
-
/** Paths this process has already complained about. See {@link loadCollectionsForCli}. */
|
|
6966
|
-
var warnedMissingCollectionsPaths = /* @__PURE__ */ new Set();
|
|
6967
|
-
async function loadCollectionsForCli(collectionsPath) {
|
|
6968
|
-
const resolvedPath = path.resolve(collectionsPath);
|
|
6969
|
-
const collections = [];
|
|
6970
|
-
if (!fs.existsSync(resolvedPath) && !warnedMissingCollectionsPaths.has(resolvedPath)) {
|
|
6971
|
-
warnedMissingCollectionsPaths.add(resolvedPath);
|
|
6972
|
-
outWarn(chalk.yellow(` ⚠ Collections path not found: "${collectionsPath}"\n Resolved to: ${resolvedPath}\n (relative to cwd: ${process.cwd()})\n Pass an absolute path, or a path relative to where you run the command.`));
|
|
6973
|
-
}
|
|
6974
|
-
if (fs.existsSync(resolvedPath)) {
|
|
6975
|
-
if (fs.statSync(resolvedPath).isDirectory()) {
|
|
6976
|
-
const files = fs.readdirSync(resolvedPath);
|
|
6977
|
-
for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") try {
|
|
6978
|
-
const module = await import(pathToFileURL(path.join(resolvedPath, file)).href);
|
|
6979
|
-
if (module && module.default) collections.push(module.default);
|
|
6980
|
-
} catch {}
|
|
6981
|
-
}
|
|
6982
|
-
}
|
|
6983
|
-
return collections;
|
|
6984
|
-
}
|
|
6985
|
-
function getDevDatabaseUrl(databaseUrl) {
|
|
6986
|
-
try {
|
|
6987
|
-
const parsed = new URL(databaseUrl);
|
|
6988
|
-
parsed.pathname = `/${parsed.pathname.slice(1)}_dev_diff`;
|
|
6989
|
-
return parsed.toString();
|
|
6990
|
-
} catch {
|
|
6991
|
-
return databaseUrl + "_dev_diff";
|
|
6992
|
-
}
|
|
6993
|
-
}
|
|
6994
|
-
/**
|
|
6995
|
-
* Create the scratch database Atlas plans against, and say so when we cannot.
|
|
6996
|
-
*
|
|
6997
|
-
* Atlas needs a `--dev-url` — an empty database it can build the desired state
|
|
6998
|
-
* in — and this makes `<db>_dev_diff` next to the real one. The whole body used
|
|
6999
|
-
* to be wrapped in an empty catch commented "Ignore, let Atlas handle
|
|
7000
|
-
* connection failures", and Atlas's version of handling it is `postgres:
|
|
7001
|
-
* querying system variables: pq: database "app_dev_diff" does not exist
|
|
7002
|
-
* (3D000)` — the symptom, four frames downstream of the cause, with the cause
|
|
7003
|
-
* thrown away.
|
|
7004
|
-
*
|
|
7005
|
-
* The cause is almost always `42501`: `CREATE DATABASE` is a privilege managed
|
|
7006
|
-
* providers do not grant, so this is the first thing a hosted user hits and the
|
|
7007
|
-
* one they can do least with.
|
|
7008
|
-
*
|
|
7009
|
-
* @returns whether the scratch database was created by *this* call — the only
|
|
7010
|
-
* case in which dropping it afterwards is ours to do.
|
|
7011
|
-
*/
|
|
7012
|
-
async function ensureDevDatabaseExists(databaseUrl, devDatabaseUrl) {
|
|
7013
|
-
let devDbName = "";
|
|
7014
|
-
try {
|
|
7015
|
-
const { Client } = await import("pg");
|
|
7016
|
-
const parsed = new URL(databaseUrl);
|
|
7017
|
-
devDbName = new URL(devDatabaseUrl).pathname.slice(1);
|
|
7018
|
-
parsed.pathname = "/postgres";
|
|
7019
|
-
const client = new Client({ connectionString: parsed.toString() });
|
|
7020
|
-
await client.connect();
|
|
7021
|
-
try {
|
|
7022
|
-
if ((await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [devDbName])).rowCount === 0) {
|
|
7023
|
-
await client.query(`CREATE DATABASE "${devDbName}"`);
|
|
7024
|
-
out(chalk.gray(` ✓ Created validation database "${devDbName}"`));
|
|
7025
|
-
return true;
|
|
7026
|
-
}
|
|
7027
|
-
} finally {
|
|
7028
|
-
await client.end();
|
|
7029
|
-
}
|
|
7030
|
-
} catch (err) {
|
|
7031
|
-
outWarn(describeDevDatabaseFailure(err, devDbName || "<db>_dev_diff"));
|
|
7032
|
-
}
|
|
7033
|
-
return false;
|
|
7034
|
-
}
|
|
7035
|
-
/**
|
|
7036
|
-
* What went wrong creating the scratch database, and what to do about it.
|
|
7037
|
-
*
|
|
7038
|
-
* Exported for its test: the `42501` branch is the one that matters and it
|
|
7039
|
-
* cannot be provoked without a non-superuser role.
|
|
7040
|
-
*/
|
|
7041
|
-
function describeDevDatabaseFailure(err, devDbName) {
|
|
7042
|
-
const code = err?.code;
|
|
7043
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7044
|
-
const lines = [chalk.yellow(` ⚠ Could not create the Atlas scratch database "${devDbName}": ${message}`)];
|
|
7045
|
-
if (code === "42501") lines.push(chalk.gray(` Atlas plans every change in a throwaway copy of the schema, and CREATE DATABASE
|
|
7046
|
-
is a privilege most managed providers withhold. Two ways forward:
|
|
7047
|
-
• GRANT the role CREATEDB: ALTER ROLE "<role>" CREATEDB;
|
|
7048
|
-
• or create it once by hand: CREATE DATABASE "${devDbName}";`));
|
|
7049
|
-
else lines.push(chalk.gray(" Atlas will fail next with `database \"" + devDbName + "\" does not exist (3D000)`, which is this."));
|
|
7050
|
-
return lines.join("\n");
|
|
7051
|
-
}
|
|
7052
|
-
/**
|
|
7053
|
-
* Drop the scratch database again, after the command that needed it succeeded.
|
|
7054
|
-
*
|
|
7055
|
-
* It is a full copy of the schema — no rows, but every table, type and index —
|
|
7056
|
-
* and it was left behind per target forever. The only notice anyone got was
|
|
7057
|
-
* `rebase db branch prune` reporting "3 Atlas scratch database(s) left over
|
|
7058
|
-
* from db push", and on a provider that bills per database it is a silent cost.
|
|
7059
|
-
*
|
|
7060
|
-
* Deliberately NOT called on the failure path: after a failed push the scratch
|
|
7061
|
-
* database holds the state Atlas was planning against, which is the one thing
|
|
7062
|
-
* worth inspecting.
|
|
7063
|
-
*/
|
|
7064
|
-
async function dropDevDatabase(databaseUrl, devDatabaseUrl) {
|
|
7065
|
-
try {
|
|
7066
|
-
const { Client } = await import("pg");
|
|
7067
|
-
const parsed = new URL(databaseUrl);
|
|
7068
|
-
const devDbName = new URL(devDatabaseUrl).pathname.slice(1);
|
|
7069
|
-
parsed.pathname = "/postgres";
|
|
7070
|
-
const client = new Client({ connectionString: parsed.toString() });
|
|
7071
|
-
await client.connect();
|
|
7072
|
-
try {
|
|
7073
|
-
await client.query(`DROP DATABASE IF EXISTS "${devDbName}"`);
|
|
7074
|
-
out(chalk.gray(` ✓ Removed validation database "${devDbName}"`));
|
|
7075
|
-
} finally {
|
|
7076
|
-
await client.end();
|
|
7077
|
-
}
|
|
7078
|
-
} catch (err) {
|
|
7079
|
-
outWarn(chalk.gray(` ⚠ Could not remove the Atlas scratch database: ${err instanceof Error ? err.message : String(err)}\n \`rebase db branch prune --include-dev-diff\` clears them.`));
|
|
7080
|
-
}
|
|
7081
|
-
}
|
|
7082
|
-
/**
|
|
7083
|
-
* The generated SQL for the project's `search` blocks, if it has any.
|
|
7084
|
-
*
|
|
7085
|
-
* @param sqlDir directory holding the generated SQL. Defaults to
|
|
7086
|
-
* `.rebase/sql` under the working directory.
|
|
7087
|
-
*/
|
|
7088
|
-
function readSearchDdl(sqlDir = generatedSqlDir()) {
|
|
7089
|
-
const searchFile = path.join(sqlDir, "search.sql");
|
|
7090
|
-
if (!fs.existsSync(searchFile)) return "";
|
|
7091
|
-
return fs.readFileSync(searchFile, "utf-8").trim();
|
|
7092
|
-
}
|
|
7093
|
-
/**
|
|
7094
|
-
* Bring the search column, its index and their helpers up to date.
|
|
7095
|
-
*
|
|
7096
|
-
* Runs *after* Atlas, not before: the statements are `ALTER TABLE ... ADD
|
|
7097
|
-
* COLUMN`, so the table has to exist. Atlas is told to ignore these objects
|
|
7098
|
-
* entirely (`getSearchExcludes`) — it cannot manage them, and left to itself it
|
|
7099
|
-
* would plan a `DROP COLUMN` for every one, since they are absent from the
|
|
7100
|
-
* desired state it was given.
|
|
7101
|
-
*
|
|
7102
|
-
* A no-op when no collection declared `search`. A failure is *not* swallowed:
|
|
7103
|
-
* silently pushing a schema whose search column never appeared is how a
|
|
7104
|
-
* collection ends up with search configured, no error anywhere, and no results.
|
|
7105
|
-
*/
|
|
7106
|
-
async function applySearchDdl(databaseUrl, sqlDir = generatedSqlDir()) {
|
|
7107
|
-
const sql = readSearchDdl(sqlDir);
|
|
7108
|
-
if (!sql) return;
|
|
7109
|
-
const { Client } = await import("pg");
|
|
7110
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7111
|
-
await client.connect();
|
|
7112
|
-
try {
|
|
7113
|
-
await client.query(sql);
|
|
7114
|
-
} finally {
|
|
7115
|
-
await client.end();
|
|
7116
|
-
}
|
|
7117
|
-
out(chalk.gray(" ✓ Applied full-text search columns and indexes"));
|
|
7118
|
-
}
|
|
7119
|
-
/**
|
|
7120
|
-
* Glob patterns keeping Atlas away from the search objects.
|
|
7121
|
-
*
|
|
7122
|
-
* Returns an empty list — and so changes nothing — for a project with no
|
|
7123
|
-
* `search` block, which is every project that has not opted in.
|
|
7124
|
-
*/
|
|
7125
|
-
async function getSearchExcludes(collectionsPath) {
|
|
7126
|
-
const { searchExcludePatterns } = await import("./generate-postgres-ddl-logic-BJsLaVNX.js").then((n) => n.o);
|
|
7127
|
-
return searchExcludePatterns(await loadCollectionsForCli(collectionsPath));
|
|
7128
|
-
}
|
|
7129
|
-
/**
|
|
7130
|
-
* Every `generated column → column it reads` edge in the database.
|
|
7131
|
-
*
|
|
7132
|
-
* Read from `pg_depend` rather than by matching names in the expression text:
|
|
7133
|
-
* a generated column's `pg_attrdef` row carries one `deptype = 'n'` dependency
|
|
7134
|
-
* per column its expression references, which is the catalogue's own answer and
|
|
7135
|
-
* cannot be fooled by a column name that also appears inside a string literal
|
|
7136
|
-
* or as part of a longer identifier.
|
|
7137
|
-
*
|
|
7138
|
-
* Used by `db push` to find what has to come out of the way before Atlas runs —
|
|
7139
|
-
* see `generated-column-conflicts.ts` for why that is necessary at all.
|
|
7140
|
-
*/
|
|
7141
|
-
async function queryGeneratedColumnDependencies(databaseUrl) {
|
|
7142
|
-
const { Client } = await import("pg");
|
|
7143
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7144
|
-
await client.connect();
|
|
7145
|
-
try {
|
|
7146
|
-
return (await client.query(`
|
|
7147
|
-
SELECT n.nspname AS "schema",
|
|
7148
|
-
c.relname AS "table",
|
|
7149
|
-
gen.attname AS "column",
|
|
7150
|
-
dep.attname AS "dependsOn"
|
|
7151
|
-
FROM pg_attrdef ad
|
|
7152
|
-
JOIN pg_class c ON c.oid = ad.adrelid
|
|
7153
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
7154
|
-
JOIN pg_attribute gen ON gen.attrelid = ad.adrelid AND gen.attnum = ad.adnum
|
|
7155
|
-
JOIN pg_depend d ON d.classid = 'pg_attrdef'::regclass
|
|
7156
|
-
AND d.objid = ad.oid
|
|
7157
|
-
AND d.refclassid = 'pg_class'::regclass
|
|
7158
|
-
AND d.refobjid = ad.adrelid
|
|
7159
|
-
AND d.deptype = 'n'
|
|
7160
|
-
JOIN pg_attribute dep ON dep.attrelid = ad.adrelid AND dep.attnum = d.refobjsubid
|
|
7161
|
-
WHERE gen.attgenerated <> ''
|
|
7162
|
-
AND NOT gen.attisdropped
|
|
7163
|
-
AND NOT dep.attisdropped;
|
|
7164
|
-
`)).rows;
|
|
7165
|
-
} finally {
|
|
7166
|
-
await client.end();
|
|
7167
|
-
}
|
|
7168
|
-
}
|
|
7169
|
-
/**
|
|
7170
|
-
* Every column in the database with its current type, for the destructive
|
|
7171
|
-
* gate to tell a widening `ALTER COLUMN … TYPE` from one that loses data.
|
|
7172
|
-
*/
|
|
7173
|
-
async function queryColumnTypes(databaseUrl) {
|
|
7174
|
-
const { Client } = await import("pg");
|
|
7175
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7176
|
-
await client.connect();
|
|
7177
|
-
try {
|
|
7178
|
-
return (await client.query(COLUMN_TYPES_SQL)).rows;
|
|
7179
|
-
} finally {
|
|
7180
|
-
await client.end();
|
|
7181
|
-
}
|
|
7182
|
-
}
|
|
7183
|
-
/**
|
|
7184
|
-
* Drop the generated columns standing in the way of an apply.
|
|
7185
|
-
*
|
|
7186
|
-
* Their indexes and their `COMMENT ON COLUMN` stamp go with them, which is
|
|
7187
|
-
* exactly right: `applySearchDdl` rebuilds all three from `search.sql` and a
|
|
7188
|
-
* surviving stamp would make the rebuilt column look like drift.
|
|
7189
|
-
*/
|
|
7190
|
-
async function dropGeneratedColumns(databaseUrl, statements) {
|
|
7191
|
-
if (statements.length === 0) return;
|
|
7192
|
-
const { Client } = await import("pg");
|
|
7193
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7194
|
-
await client.connect();
|
|
7195
|
-
try {
|
|
7196
|
-
for (const statement of statements) await client.query(statement);
|
|
7197
|
-
} finally {
|
|
7198
|
-
await client.end();
|
|
7199
|
-
}
|
|
7200
|
-
}
|
|
7201
|
-
/**
|
|
7202
|
-
* The generated SQL for the project's `vector` properties, if it has any.
|
|
7203
|
-
*
|
|
7204
|
-
* @param sqlDir directory holding the generated SQL. Defaults to
|
|
7205
|
-
* `.rebase/sql` under the working directory.
|
|
7206
|
-
*/
|
|
7207
|
-
function readVectorDdl(sqlDir = generatedSqlDir()) {
|
|
7208
|
-
const vectorFile = path.join(sqlDir, "vector.sql");
|
|
7209
|
-
if (!fs.existsSync(vectorFile)) return "";
|
|
7210
|
-
return fs.readFileSync(vectorFile, "utf-8").trim();
|
|
7211
|
-
}
|
|
7212
|
-
/**
|
|
7213
|
-
* Install pgvector and bring the vector columns and their ANN indexes up to
|
|
7214
|
-
* date.
|
|
7215
|
-
*
|
|
7216
|
-
* Runs *after* Atlas, like {@link applySearchDdl} and for the same reason: the
|
|
7217
|
-
* statements are `ALTER TABLE … ADD COLUMN`, so the table has to exist. Atlas
|
|
7218
|
-
* is told to ignore these objects entirely ({@link getVectorExcludes}) — it
|
|
7219
|
-
* cannot manage a type its dev database is structurally incapable of having,
|
|
7220
|
-
* and left to itself it would plan a `DROP COLUMN` for every one.
|
|
7221
|
-
*
|
|
7222
|
-
* A no-op when no collection declares a `vector` property. A failure is *not*
|
|
7223
|
-
* swallowed: a push that reported success while the embedding column never
|
|
7224
|
-
* appeared is how a project ends up with vector search configured, no error
|
|
7225
|
-
* anywhere, and no results.
|
|
7226
|
-
*/
|
|
7227
|
-
async function applyVectorDdl(databaseUrl, sqlDir = generatedSqlDir()) {
|
|
7228
|
-
const sql = readVectorDdl(sqlDir);
|
|
7229
|
-
if (!sql) return;
|
|
7230
|
-
const { vectorExtensionHint } = await import("./vector-index-CMJKb3C9.js").then((n) => n.f);
|
|
7231
|
-
const { Client } = await import("pg");
|
|
7232
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7233
|
-
await client.connect();
|
|
7234
|
-
try {
|
|
7235
|
-
await client.query(sql);
|
|
7236
|
-
} catch (err) {
|
|
7237
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7238
|
-
const hint = vectorExtensionHint(message);
|
|
7239
|
-
if (!hint) throw err;
|
|
7240
|
-
throw new Error(`${message}${hint}`, { cause: err });
|
|
7241
|
-
} finally {
|
|
7242
|
-
await client.end();
|
|
7243
|
-
}
|
|
7244
|
-
out(chalk.gray(" ✓ Applied vector columns and ANN indexes"));
|
|
7245
|
-
}
|
|
7246
|
-
/**
|
|
7247
|
-
* Glob patterns keeping Atlas away from the vector objects.
|
|
7248
|
-
*
|
|
7249
|
-
* Returns an empty list — and so changes nothing — for a project with no
|
|
7250
|
-
* `vector` property, which is every project that has not opted in.
|
|
7251
|
-
*/
|
|
7252
|
-
async function getVectorExcludes(collectionsPath) {
|
|
7253
|
-
const { vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-BJsLaVNX.js").then((n) => n.o);
|
|
7254
|
-
return vectorExcludePatterns(await loadCollectionsForCli(collectionsPath));
|
|
7255
|
-
}
|
|
7256
|
-
/**
|
|
7257
|
-
* The generated SQL for the project's `autoValue: "on_update"` properties, if
|
|
7258
|
-
* it has any.
|
|
7259
|
-
*
|
|
7260
|
-
* @param sqlDir directory holding the generated SQL. Defaults to
|
|
7261
|
-
* `.rebase/sql` under the working directory.
|
|
7262
|
-
*/
|
|
7263
|
-
function readTriggersDdl(sqlDir = generatedSqlDir()) {
|
|
7264
|
-
const triggersFile = path.join(sqlDir, "triggers.sql");
|
|
7265
|
-
if (!fs.existsSync(triggersFile)) return "";
|
|
7266
|
-
return fs.readFileSync(triggersFile, "utf-8").trim();
|
|
7267
|
-
}
|
|
7268
|
-
/**
|
|
7269
|
-
* Install the `updated_at` trigger function and bind it to every column that
|
|
7270
|
-
* asked for one.
|
|
7271
|
-
*
|
|
7272
|
-
* Runs *after* Atlas, like {@link applySearchDdl} and {@link applyVectorDdl}:
|
|
7273
|
-
* a trigger needs its table and its column to exist. Atlas is told to ignore
|
|
7274
|
-
* the triggers ({@link getTriggerExcludes}) because it cannot manage a function
|
|
7275
|
-
* at all — its free tier refuses to parse a desired state containing one.
|
|
7276
|
-
*
|
|
7277
|
-
* A no-op when no property declares `autoValue: "on_update"`. A failure is
|
|
7278
|
-
* *not* swallowed: a push that reported success while the trigger never
|
|
7279
|
-
* appeared is how `updated_at` ends up silently stale for every writer that is
|
|
7280
|
-
* not the driver, which is the whole reason the trigger exists.
|
|
7281
|
-
*/
|
|
7282
|
-
async function applyTriggersDdl(databaseUrl, sqlDir = generatedSqlDir()) {
|
|
7283
|
-
const sql = readTriggersDdl(sqlDir);
|
|
7284
|
-
if (!sql) return;
|
|
7285
|
-
const { Client } = await import("pg");
|
|
7286
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7287
|
-
await client.connect();
|
|
7288
|
-
try {
|
|
7289
|
-
await client.query(sql);
|
|
7290
|
-
} finally {
|
|
7291
|
-
await client.end();
|
|
7292
|
-
}
|
|
7293
|
-
out(chalk.gray(" ✓ Applied `updated_at` triggers"));
|
|
7294
|
-
}
|
|
7295
|
-
/** @see getSearchExcludes */
|
|
7296
|
-
async function getTriggerExcludes(collectionsPath) {
|
|
7297
|
-
const { triggerExcludePatterns } = await import("./generate-postgres-ddl-logic-BJsLaVNX.js").then((n) => n.o);
|
|
7298
|
-
return triggerExcludePatterns(await loadCollectionsForCli(collectionsPath));
|
|
7299
|
-
}
|
|
7300
|
-
/**
|
|
7301
|
-
* Query the live database for every user table/view outside the system
|
|
7302
|
-
* catalogs. Separated from {@link getTableExcludes} so its failure mode can
|
|
7303
|
-
* be handled explicitly (fail closed) and so tests can inject a stub.
|
|
7304
|
-
*/
|
|
7305
|
-
/**
|
|
7306
|
-
* Every index Postgres holds on a table Rebase manages, as
|
|
7307
|
-
* `schema.table.index`.
|
|
7308
|
-
*
|
|
7309
|
-
* Constraint-backed indexes are left out: a `PRIMARY KEY` or a `UNIQUE` is a
|
|
7310
|
-
* *constraint* to Atlas, diffed from the constraint declaration, and naming its
|
|
7311
|
-
* index in an exclude would shield the constraint itself.
|
|
7312
|
-
*/
|
|
7313
|
-
async function queryExistingIndexes(databaseUrl) {
|
|
7314
|
-
const { Client } = await import("pg");
|
|
7315
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7316
|
-
await client.connect();
|
|
7317
|
-
try {
|
|
7318
|
-
return (await client.query(`
|
|
7319
|
-
SELECT n.nspname || '.' || t.relname || '.' || i.relname AS full_name
|
|
7320
|
-
FROM pg_index x
|
|
7321
|
-
JOIN pg_class i ON i.oid = x.indexrelid
|
|
7322
|
-
JOIN pg_class t ON t.oid = x.indrelid
|
|
7323
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
7324
|
-
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
7325
|
-
AND t.relkind IN ('r', 'p')
|
|
7326
|
-
AND NOT EXISTS (
|
|
7327
|
-
SELECT 1 FROM pg_constraint c WHERE c.conindid = i.oid
|
|
7328
|
-
);
|
|
7329
|
-
`)).rows.map((row) => row.full_name);
|
|
7330
|
-
} finally {
|
|
7331
|
-
await client.end();
|
|
7332
|
-
}
|
|
7333
|
-
}
|
|
7334
|
-
/**
|
|
7335
|
-
* Glob patterns keeping Atlas away from indexes that are not Rebase's.
|
|
7336
|
-
*
|
|
7337
|
-
* The problem this solves is live and predates the `indexes:` block. `db push`
|
|
7338
|
-
* is declarative, so an index on a managed table that is absent from
|
|
7339
|
-
* `schema.sql` is drift — and Atlas plans `DROP INDEX` for it. Verified
|
|
7340
|
-
* against atlas v1.2.3: create one by hand, re-run an unchanged push, and the
|
|
7341
|
-
* plan is a bare drop. `DROP INDEX` is not in `DESTRUCTIVE_PATTERNS`, so the
|
|
7342
|
-
* auto-approved apply took it without asking. Until now the only way to have
|
|
7343
|
-
* an index at all was to write it by hand, which made that the *only* outcome.
|
|
7344
|
-
*
|
|
7345
|
-
* Ownership rather than a prompt, because once indexes are declarable a drop
|
|
7346
|
-
* is usually correct: removing one from the config should remove it from the
|
|
7347
|
-
* database, quietly. What must never be dropped is an index Rebase never
|
|
7348
|
-
* created — a hand-written one, or one an introspected database arrived with.
|
|
7349
|
-
* {@link isRebaseIndexName} is the test, the same arrangement
|
|
7350
|
-
* `isGeneratedPolicyName` uses to let policy reconciliation drop only what it
|
|
7351
|
-
* generated.
|
|
7352
|
-
*
|
|
7353
|
-
* The named-in-the-desired-state check comes first and is what makes removal
|
|
7354
|
-
* work: a declared index that the author has just deleted still matches the
|
|
7355
|
-
* name pattern, is no longer in the plan, and so is *not* excluded — Atlas
|
|
7356
|
-
* drops it, as intended.
|
|
7357
|
-
*
|
|
7358
|
-
* Three-part `schema.table.index`, never two: a two-part pattern reads as a
|
|
7359
|
-
* *table* named `<index>` in a *schema* named `<table>`, matches nothing, and
|
|
7360
|
-
* reports no error — the trap already recorded for the search excludes.
|
|
7361
|
-
*/
|
|
7362
|
-
async function getForeignIndexExcludes(databaseUrl, collectionsPath, deps = {}) {
|
|
7363
|
-
const queryIndexes = deps.queryExistingIndexes ?? queryExistingIndexes;
|
|
7364
|
-
const managed = await (deps.getManagedIndexNames ?? managedIndexNames)(collectionsPath);
|
|
7365
|
-
let existing;
|
|
7366
|
-
try {
|
|
7367
|
-
existing = await queryIndexes(databaseUrl);
|
|
7368
|
-
} catch (err) {
|
|
7369
|
-
throw new ExcludeIntrospectionError(`Failed to introspect the database for unmanaged indexes: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
7370
|
-
}
|
|
7371
|
-
return existing.filter((full) => {
|
|
7372
|
-
const indexName = full.slice(full.lastIndexOf(".") + 1);
|
|
7373
|
-
if (managed.has(indexName)) return false;
|
|
7374
|
-
return !isRebaseIndexName(indexName);
|
|
7375
|
-
});
|
|
7376
|
-
}
|
|
7377
|
-
/**
|
|
7378
|
-
* The index names the current collections would create — search, vector and
|
|
7379
|
-
* declared alike. Anything Atlas would emit is by definition not foreign.
|
|
7380
|
-
*/
|
|
7381
|
-
async function managedIndexNames(collectionsPath) {
|
|
7382
|
-
const collections = await loadCollectionsForCli(collectionsPath);
|
|
7383
|
-
const { searchExcludePatterns, vectorExcludePatterns } = await import("./generate-postgres-ddl-logic-BJsLaVNX.js").then((n) => n.o);
|
|
7384
|
-
const { resolveColumnName } = await import("./column-plan-helpers-1-LQD0yI.js").then((n) => n.t);
|
|
7385
|
-
const { buildCollectionIndexPlan } = await import("./collection-index-DdnsxiJ_.js").then((n) => n.i);
|
|
7386
|
-
const names = new Set(buildCollectionIndexPlan(collections, resolveColumnName).map((spec) => spec.indexName));
|
|
7387
|
-
for (const pattern of [...searchExcludePatterns(collections), ...vectorExcludePatterns(collections)]) names.add(pattern.slice(pattern.lastIndexOf(".") + 1));
|
|
7388
|
-
return names;
|
|
7389
|
-
}
|
|
7390
|
-
async function queryExistingTables(databaseUrl) {
|
|
7391
|
-
const { Client } = await import("pg");
|
|
7392
|
-
const client = new Client({ connectionString: databaseUrl });
|
|
7393
|
-
await client.connect();
|
|
7394
|
-
try {
|
|
7395
|
-
return (await client.query(`
|
|
7396
|
-
SELECT table_schema || '.' || table_name AS full_name
|
|
7397
|
-
FROM information_schema.tables
|
|
7398
|
-
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
7399
|
-
AND table_type IN ('BASE TABLE', 'VIEW');
|
|
7400
|
-
`)).rows.map((row) => row.full_name);
|
|
7401
|
-
} finally {
|
|
7402
|
-
await client.end();
|
|
7403
|
-
}
|
|
7404
|
-
}
|
|
7405
|
-
/**
|
|
7406
|
-
* Raised when the exclude list could not be built. `db push` MUST abort on
|
|
7407
|
-
* this rather than continue: the exclude list is the only thing shielding
|
|
7408
|
-
* non-collection (user/system) tables from the auto-approved declarative
|
|
7409
|
-
* apply. A partial list — the old fail-open behaviour — meant a transient
|
|
7410
|
-
* introspection hiccup dropped every table not present in `schema.sql`.
|
|
7411
|
-
*/
|
|
7412
|
-
var ExcludeIntrospectionError = class extends Error {
|
|
7413
|
-
cause;
|
|
7414
|
-
constructor(message, cause) {
|
|
7415
|
-
super(message);
|
|
7416
|
-
this.cause = cause;
|
|
7417
|
-
this.name = "ExcludeIntrospectionError";
|
|
7418
|
-
}
|
|
7419
|
-
};
|
|
7420
|
-
/**
|
|
7421
|
-
* Build the `--exclude` list that protects tables Rebase doesn't manage from
|
|
7422
|
-
* the declarative apply. Anything not backing a collection (or its M2M
|
|
7423
|
-
* junctions) is excluded so Atlas never drops it.
|
|
7424
|
-
*
|
|
7425
|
-
* Fails **closed**: if the database can't be introspected we cannot know
|
|
7426
|
-
* which tables to protect, so we throw {@link ExcludeIntrospectionError}
|
|
7427
|
-
* instead of returning a near-empty list and letting the caller drop
|
|
7428
|
-
* everything.
|
|
7429
|
-
*
|
|
7430
|
-
* `deps` is injectable for tests; production uses the real pg-backed queries.
|
|
7431
|
-
*/
|
|
7432
|
-
async function getTableExcludes(databaseUrl, collectionsPath, deps = {}) {
|
|
7433
|
-
const getIncludes = deps.getIncludes ?? getTableIncludes;
|
|
7434
|
-
const queryTables = deps.queryExistingTables ?? queryExistingTables;
|
|
7435
|
-
const includes = await getIncludes(collectionsPath);
|
|
7436
|
-
const excludes = [
|
|
7437
|
-
"atlas_schema_revisions.*",
|
|
7438
|
-
"auth",
|
|
7439
|
-
"auth.*",
|
|
7440
|
-
"rebase",
|
|
7441
|
-
"rebase.*"
|
|
7442
|
-
];
|
|
7443
|
-
let existingTables;
|
|
7444
|
-
try {
|
|
7445
|
-
existingTables = await queryTables(databaseUrl);
|
|
7446
|
-
} catch (err) {
|
|
7447
|
-
throw new ExcludeIntrospectionError(`Failed to introspect the database for unmapped tables: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
7448
|
-
}
|
|
7449
|
-
for (const table of existingTables) if (!includes.includes(table)) excludes.push(table);
|
|
7450
|
-
return excludes;
|
|
7451
|
-
}
|
|
7452
|
-
/**
|
|
7453
|
-
* Ask a yes/no question on an interactive terminal.
|
|
7454
|
-
*
|
|
7455
|
-
* The `isTTY` guard is the contract, not an optimisation: non-interactive
|
|
7456
|
-
* shells (CI, pipes, agents) can never answer, and `readline` on a
|
|
7457
|
-
* non-TTY stdin resolves with whatever the pipe happens to contain — or never
|
|
7458
|
-
* resolves at all. Returning false there is what makes an unattended
|
|
7459
|
-
* `db push` abort instead of auto-confirming a destructive change. Callers
|
|
7460
|
-
* should already have gone through {@link decidePushSafety}, which decides
|
|
7461
|
-
* whether interactive confirmation is even possible; this is the backstop.
|
|
7462
|
-
*
|
|
7463
|
-
* Lives here rather than in cli.ts so it is reachable from a test — cli.ts uses
|
|
7464
|
-
* `import.meta` and cannot be imported by the jest runner.
|
|
7465
|
-
*/
|
|
7466
|
-
async function promptConfirm(question) {
|
|
7467
|
-
if (!process.stdin.isTTY) return false;
|
|
7468
|
-
const rl = import___vite_browser_external.default.createInterface({
|
|
7469
|
-
input: process.stdin,
|
|
7470
|
-
output: process.stdout
|
|
7471
|
-
});
|
|
7472
|
-
try {
|
|
7473
|
-
const answer = await new Promise((resolve) => rl.question(question, resolve));
|
|
7474
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
7475
|
-
} finally {
|
|
7476
|
-
rl.close();
|
|
7477
|
-
}
|
|
7478
|
-
}
|
|
7479
|
-
//#endregion
|
|
7480
6247
|
//#region src/schema/rls-bootstrap-sql.ts
|
|
7481
6248
|
var rls_bootstrap_sql_exports = /* @__PURE__ */ __exportAll({
|
|
7482
6249
|
DROP_LEGACY_AUTH_SCHEMA_SQL: () => DROP_LEGACY_AUTH_SCHEMA_SQL,
|
|
@@ -7484,7 +6251,8 @@ var rls_bootstrap_sql_exports = /* @__PURE__ */ __exportAll({
|
|
|
7484
6251
|
LEGACY_RLS_FUNCTION_DEPENDENTS_SQL: () => LEGACY_RLS_FUNCTION_DEPENDENTS_SQL,
|
|
7485
6252
|
RLS_BOOTSTRAP_SQL: () => RLS_BOOTSTRAP_SQL,
|
|
7486
6253
|
RLS_BOOTSTRAP_STATEMENTS: () => RLS_BOOTSTRAP_STATEMENTS,
|
|
7487
|
-
dropLegacyAuthSchema: () => dropLegacyAuthSchema
|
|
6254
|
+
dropLegacyAuthSchema: () => dropLegacyAuthSchema,
|
|
6255
|
+
rlsRuntimeIsCurrent: () => rlsRuntimeIsCurrent
|
|
7488
6256
|
});
|
|
7489
6257
|
/**
|
|
7490
6258
|
* Canonical SQL bootstrap for the RLS helper functions.
|
|
@@ -7553,6 +6321,93 @@ $$ LANGUAGE sql STABLE`,
|
|
|
7553
6321
|
SELECT COALESCE(NULLIF(current_setting('app.is_anonymous', true), ''), 'false')::boolean;
|
|
7554
6322
|
$$ LANGUAGE sql STABLE`
|
|
7555
6323
|
];
|
|
6324
|
+
/** Everything `CREATE OR REPLACE FUNCTION` sets on a function, as one comparable string. */
|
|
6325
|
+
var describeFunction = (facts) => JSON.stringify([
|
|
6326
|
+
facts.returns.toLowerCase(),
|
|
6327
|
+
facts.language.toLowerCase(),
|
|
6328
|
+
facts.volatility,
|
|
6329
|
+
facts.strict,
|
|
6330
|
+
facts.parallel,
|
|
6331
|
+
facts.securityDefiner,
|
|
6332
|
+
facts.leakproof,
|
|
6333
|
+
facts.cost,
|
|
6334
|
+
facts.hasConfig,
|
|
6335
|
+
facts.body
|
|
6336
|
+
]);
|
|
6337
|
+
/**
|
|
6338
|
+
* What each `CREATE OR REPLACE FUNCTION` in {@link RLS_BOOTSTRAP_STATEMENTS}
|
|
6339
|
+
* would leave in the catalogue, by `schema.name`. Parsed
|
|
6340
|
+
* from the statements themselves, so the two cannot drift; a statement this
|
|
6341
|
+
* does not recognise makes {@link rlsRuntimeIsCurrent} answer "no", and the
|
|
6342
|
+
* bootstrap runs as it always did.
|
|
6343
|
+
*/
|
|
6344
|
+
var expectedRlsFunctions = () => {
|
|
6345
|
+
const expected = /* @__PURE__ */ new Map();
|
|
6346
|
+
for (const statement of RLS_BOOTSTRAP_STATEMENTS) {
|
|
6347
|
+
if (/^CREATE SCHEMA IF NOT EXISTS /.test(statement)) continue;
|
|
6348
|
+
const match = /^CREATE OR REPLACE FUNCTION (\w+)\.(\w+)\(\) RETURNS (\w+) AS \$\$([\s\S]*)\$\$ LANGUAGE (\w+) (IMMUTABLE|STABLE|VOLATILE)$/.exec(statement);
|
|
6349
|
+
if (!match) return void 0;
|
|
6350
|
+
const [, schema, name, returns, body, language, volatility] = match;
|
|
6351
|
+
expected.set(`${schema}.${name}`, describeFunction({
|
|
6352
|
+
returns,
|
|
6353
|
+
language,
|
|
6354
|
+
volatility: volatility[0].toLowerCase(),
|
|
6355
|
+
strict: false,
|
|
6356
|
+
parallel: "u",
|
|
6357
|
+
securityDefiner: false,
|
|
6358
|
+
leakproof: false,
|
|
6359
|
+
cost: 100,
|
|
6360
|
+
hasConfig: false,
|
|
6361
|
+
body
|
|
6362
|
+
}));
|
|
6363
|
+
}
|
|
6364
|
+
return expected;
|
|
6365
|
+
};
|
|
6366
|
+
/**
|
|
6367
|
+
* Whether the `rebase` schema and every RLS helper function are exactly what
|
|
6368
|
+
* {@link RLS_BOOTSTRAP_STATEMENTS} would make them — one catalogue read.
|
|
6369
|
+
*
|
|
6370
|
+
* The boot ran those statements unconditionally, on every boot and on every
|
|
6371
|
+
* source: four `CREATE OR REPLACE FUNCTION`s in a transaction under an
|
|
6372
|
+
* advisory lock. Replacing a function rewrites its `pg_proc` row even when
|
|
6373
|
+
* nothing changed, and every cached plan that inlined it — every plan of every
|
|
6374
|
+
* statement a policy applies to, on every connection — is invalidated by it.
|
|
6375
|
+
* A function that differs in anything `CREATE OR REPLACE` sets (body,
|
|
6376
|
+
* language, volatility, strictness, parallel safety, security, leakproofness,
|
|
6377
|
+
* cost, `SET` options), or is missing, answers `false`, and the statements run
|
|
6378
|
+
* as before.
|
|
6379
|
+
*/
|
|
6380
|
+
async function rlsRuntimeIsCurrent(run) {
|
|
6381
|
+
const expected = expectedRlsFunctions();
|
|
6382
|
+
if (!expected) return false;
|
|
6383
|
+
const rows = await run(`SELECT n.nspname AS schema, p.proname AS name, pg_get_function_identity_arguments(p.oid) AS args,
|
|
6384
|
+
format_type(p.prorettype, NULL) AS returns, l.lanname AS language,
|
|
6385
|
+
p.provolatile::text AS volatility, p.proisstrict AS strict, p.proparallel::text AS parallel,
|
|
6386
|
+
p.prosecdef AS security_definer, p.proleakproof AS leakproof, p.procost AS cost,
|
|
6387
|
+
p.proconfig IS NOT NULL AS has_config, p.prosrc AS body
|
|
6388
|
+
FROM pg_proc p
|
|
6389
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
6390
|
+
JOIN pg_language l ON p.prolang = l.oid
|
|
6391
|
+
WHERE (n.nspname, p.proname) IN (${[...expected.keys()].map((key) => key.split(".")).map(([schema, name]) => `('${schema}', '${name}')`).join(", ")})`);
|
|
6392
|
+
const live = /* @__PURE__ */ new Map();
|
|
6393
|
+
for (const row of rows) {
|
|
6394
|
+
if (String(row.args ?? "") !== "") continue;
|
|
6395
|
+
live.set(`${row.schema}.${row.name}`, describeFunction({
|
|
6396
|
+
returns: String(row.returns),
|
|
6397
|
+
language: String(row.language),
|
|
6398
|
+
volatility: String(row.volatility),
|
|
6399
|
+
strict: row.strict === true,
|
|
6400
|
+
parallel: String(row.parallel),
|
|
6401
|
+
securityDefiner: row.security_definer === true,
|
|
6402
|
+
leakproof: row.leakproof === true,
|
|
6403
|
+
cost: Number(row.cost),
|
|
6404
|
+
hasConfig: row.has_config === true,
|
|
6405
|
+
body: String(row.body)
|
|
6406
|
+
}));
|
|
6407
|
+
}
|
|
6408
|
+
for (const [key, definition] of expected) if (live.get(key) !== definition) return false;
|
|
6409
|
+
return true;
|
|
6410
|
+
}
|
|
7556
6411
|
/** The same statements as one script, for migration files and raw clients. */
|
|
7557
6412
|
var RLS_BOOTSTRAP_SQL = "-- Rebase schema + RLS helper functions (required by the policies below)\n" + RLS_BOOTSTRAP_STATEMENTS.map((s) => `${s};`).join("\n\n") + "\n";
|
|
7558
6413
|
/**
|
|
@@ -7718,6 +6573,6 @@ async function dropLegacyAuthSchema(run, report) {
|
|
|
7718
6573
|
}
|
|
7719
6574
|
}
|
|
7720
6575
|
//#endregion
|
|
7721
|
-
export {
|
|
6576
|
+
export { rls_bootstrap_sql_exports as a, rlsRuntimeIsCurrent as i, RLS_BOOTSTRAP_STATEMENTS as n, execa as o, dropLegacyAuthSchema as r, forLibpq as s, RLS_BOOTSTRAP_SQL as t };
|
|
7722
6577
|
|
|
7723
|
-
//# sourceMappingURL=rls-bootstrap-sql-
|
|
6578
|
+
//# sourceMappingURL=rls-bootstrap-sql-RQUljnJ1.js.map
|