@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g56af4a4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
- package/dist/BranchService-Bsn2wfaj.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +166 -13
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +78 -14
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- 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-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
- package/dist/backup-cli-aUfRv16h.js.map +1 -0
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
- 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-errors-CaI4ntyD.js +1919 -0
- package/dist/cli-errors-CaI4ntyD.js.map +1 -0
- package/dist/cli-errors.d.ts +93 -5
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
- package/dist/cli-flags-B-Dawocl.js.map +1 -0
- package/dist/cli-helpers.d.ts +52 -12
- package/dist/cli-output.d.ts +16 -28
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli.js +625 -190
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
- package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
- package/dist/connection-BPoNILnD.js.map +1 -0
- package/dist/connection.d.ts +9 -1
- package/dist/data-transformer.d.ts +0 -8
- package/dist/databasePoolManager-BkEy2w18.js +119 -0
- package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
- package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
- package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
- package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
- 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-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
- package/dist/ensure-tables-CHsgyLqg.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-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
- 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-B64O2oZF.js +152 -0
- package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
- package/dist/generated-sql.d.ts +34 -0
- package/dist/history/HistoryService.d.ts +38 -6
- package/dist/history/ensure-history-table.d.ts +42 -4
- package/dist/index.es.js +5557 -7612
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
- package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
- package/dist/introspect-db-queries-D6kAqReI.js +339 -0
- package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
- package/dist/pg-error-utils-vJezlXdI.js +388 -0
- package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
- package/dist/plan-schema-B1YF_tYq.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
- 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/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
- package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
- package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +172 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +38 -21
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +27 -1
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +53 -1
- package/dist/schema/ensure-collection-policies.d.ts +95 -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-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +43 -0
- package/dist/schema/introspect-db-queries.d.ts +12 -2
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +17 -6
- 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 +127 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +45 -11
- 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/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +39 -4
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +63 -12
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +106 -3
- package/dist/services/cdc/CdcListener.d.ts +26 -10
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +180 -18
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +32 -5
- package/dist/services/collection-helpers.d.ts +26 -1
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +95 -6
- package/dist/services/realtimeService.d.ts +467 -53
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +85 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +275 -0
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +22 -0
- package/dist/services/write-transaction-scope.d.ts +15 -3
- package/dist/socket-liveness-D3UD5_BW.js +7570 -0
- package/dist/socket-liveness-D3UD5_BW.js.map +1 -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 +63 -3
- 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.d.ts +63 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/backup-cli-24v4OlSp.js.map +0 -1
- package/dist/backup-service-HQ9GC4tN.js.map +0 -1
- package/dist/cli-errors-B8qHg02P.js +0 -798
- package/dist/cli-errors-B8qHg02P.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/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
- package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
|
@@ -1,857 +0,0 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from "module";
|
|
2
|
-
__createRequire(import.meta.url);
|
|
3
|
-
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import { a as triggerStatements, c as SEARCH_TEXT_FN, h as searchColumnStamps, i as SET_UPDATED_AT_FN, l as SEARCH_UNACCENT_FN, n as planSchema, r as quoteSqlLiteral, y as searchIndexStatements } from "./plan-schema-C0fxM8dY.js";
|
|
5
|
-
import { d as extractPgError, u as extractCauseMessage } from "./cli-errors-B8qHg02P.js";
|
|
6
|
-
import { s as vectorExtensionHint, u as vectorIndexStatement } from "./vector-index-CMJKb3C9.js";
|
|
7
|
-
import { c as sortIndexSpecs, n as collectionIndexStatement } from "./collection-index-DdnsxiJ_.js";
|
|
8
|
-
import { c as planJunctionTables, l as renderColumnDefinition, u as renderPgType } from "./generate-postgres-ddl-logic-Bt2d2mRH.js";
|
|
9
|
-
import { v as quoteRelation } from "./introspect-db-logic-WMuAfxvw.js";
|
|
10
|
-
import { isConcurrentDdlRace, isDuplicateObjectRace, logger } from "@rebasepro/server";
|
|
11
|
-
import { REBASE_SCHEMA, declaredDatabaseExtensions } from "@rebasepro/types";
|
|
12
|
-
import { relationalCollections } from "@rebasepro/common";
|
|
13
|
-
//#region src/schema/column-type-drift.ts
|
|
14
|
-
/**
|
|
15
|
-
* Types that mean the same thing to everything above the driver.
|
|
16
|
-
*
|
|
17
|
-
* Keyed by every spelling either side can produce: `information_schema` reports
|
|
18
|
-
* `udt_name` (`int4`, `_numeric`, `bpchar`), while the generators emit SQL
|
|
19
|
-
* (`INTEGER`, `NUMERIC[]`, `CHAR(3)`). Both are normalised through here.
|
|
20
|
-
*/
|
|
21
|
-
var FAMILIES = {
|
|
22
|
-
int2: "number",
|
|
23
|
-
smallint: "number",
|
|
24
|
-
int4: "number",
|
|
25
|
-
int: "number",
|
|
26
|
-
integer: "number",
|
|
27
|
-
int8: "number",
|
|
28
|
-
bigint: "number",
|
|
29
|
-
numeric: "number",
|
|
30
|
-
decimal: "number",
|
|
31
|
-
float4: "number",
|
|
32
|
-
real: "number",
|
|
33
|
-
float8: "number",
|
|
34
|
-
"double precision": "number",
|
|
35
|
-
money: "number",
|
|
36
|
-
text: "text",
|
|
37
|
-
varchar: "text",
|
|
38
|
-
"character varying": "text",
|
|
39
|
-
bpchar: "text",
|
|
40
|
-
char: "text",
|
|
41
|
-
character: "text",
|
|
42
|
-
citext: "text",
|
|
43
|
-
bool: "boolean",
|
|
44
|
-
boolean: "boolean",
|
|
45
|
-
json: "json",
|
|
46
|
-
jsonb: "jsonb",
|
|
47
|
-
uuid: "uuid",
|
|
48
|
-
bytea: "bytea",
|
|
49
|
-
date: "date",
|
|
50
|
-
time: "time",
|
|
51
|
-
"time without time zone": "time",
|
|
52
|
-
timetz: "time",
|
|
53
|
-
"time with time zone": "time",
|
|
54
|
-
timestamp: "timestamp",
|
|
55
|
-
"timestamp without time zone": "timestamp",
|
|
56
|
-
timestamptz: "timestamptz",
|
|
57
|
-
"timestamp with time zone": "timestamptz",
|
|
58
|
-
interval: "interval",
|
|
59
|
-
tsvector: "tsvector",
|
|
60
|
-
vector: "vector",
|
|
61
|
-
halfvec: "halfvec",
|
|
62
|
-
sparsevec: "sparsevec"
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* One SQL or `udt_name` spelling reduced to its family, or `null` when this
|
|
66
|
-
* module has no opinion about it.
|
|
67
|
-
*
|
|
68
|
-
* `null` is not a failure. It is the answer for an enum type, a domain, a
|
|
69
|
-
* PostGIS column, a quoted qualified name — everything whose identity is a
|
|
70
|
-
* *name* rather than a shape, where two spellings being different says nothing
|
|
71
|
-
* about whether the values are compatible.
|
|
72
|
-
*/
|
|
73
|
-
function typeFamily(raw) {
|
|
74
|
-
if (!raw) return null;
|
|
75
|
-
let type = raw.trim().toLowerCase();
|
|
76
|
-
if (!type) return null;
|
|
77
|
-
if (type.startsWith("\"")) return null;
|
|
78
|
-
const generated = type.indexOf(" generated ");
|
|
79
|
-
if (generated > 0) type = type.slice(0, generated).trim();
|
|
80
|
-
if (type.endsWith("[]")) {
|
|
81
|
-
const element = typeFamily(type.slice(0, -2));
|
|
82
|
-
return element && `array:${element}`;
|
|
83
|
-
}
|
|
84
|
-
if (type.startsWith("_")) {
|
|
85
|
-
const element = typeFamily(type.slice(1));
|
|
86
|
-
return element && `array:${element}`;
|
|
87
|
-
}
|
|
88
|
-
const paren = type.indexOf("(");
|
|
89
|
-
if (paren > 0) type = type.slice(0, paren).trim();
|
|
90
|
-
return FAMILIES[type] ?? null;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Do these two spellings describe columns that hold the same kind of value?
|
|
94
|
-
*
|
|
95
|
-
* `true` whenever the answer is not confidently "no" — an unrecognised type on
|
|
96
|
-
* either side agrees with everything, because a report needs evidence and an
|
|
97
|
-
* unknown type is the absence of it.
|
|
98
|
-
*/
|
|
99
|
-
function typesAgree(declared, actual) {
|
|
100
|
-
const a = typeFamily(declared);
|
|
101
|
-
const b = typeFamily(actual);
|
|
102
|
-
if (a === null || b === null) return true;
|
|
103
|
-
if (a !== b) return false;
|
|
104
|
-
if (!isNumeric(declared) || !isNumeric(actual)) return true;
|
|
105
|
-
const declaredModifier = numericModifier(declared);
|
|
106
|
-
if (declaredModifier === null) return true;
|
|
107
|
-
return declaredModifier === numericModifier(actual);
|
|
108
|
-
}
|
|
109
|
-
/** `NUMERIC`, `numeric(10,2)`, `DECIMAL(10, 2)` — the arbitrary-precision type itself. */
|
|
110
|
-
function isNumeric(raw) {
|
|
111
|
-
return /^\s*(?:numeric|decimal)\s*(?:\(|$)/i.test(raw);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* `numeric(10,2)` / `NUMERIC(10, 2)` → `10,2`; a bare `numeric` → `null`. A
|
|
115
|
-
* precision alone means a scale of 0, which is how the catalogue reports it:
|
|
116
|
-
* `NUMERIC(10)` → `10,0`.
|
|
117
|
-
*/
|
|
118
|
-
function numericModifier(raw) {
|
|
119
|
-
const match = raw.match(/\(([^)]*)\)/);
|
|
120
|
-
if (!match) return null;
|
|
121
|
-
const [precision, scale = "0"] = match[1].split(",").map((part) => part.trim());
|
|
122
|
-
return `${precision},${scale}`;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* The sentence a boot log says about one drifted column.
|
|
126
|
-
*
|
|
127
|
-
* Written to be actionable in the place it is read, which is a runtime log with
|
|
128
|
-
* no way to run a migration from it: it names both types, says which surface
|
|
129
|
-
* will break first, and gives the statement that resolves it.
|
|
130
|
-
*/
|
|
131
|
-
function columnTypeDriftMessage(drift) {
|
|
132
|
-
const [schema, table] = drift.table.split(".");
|
|
133
|
-
return `Column "${drift.table}"."${drift.column}" is ${drift.actual} in the database, but this collection declares ${drift.declared}. Boot does not change an existing column's type, so the database keeps what it has and writes that match the declaration will be rejected by Postgres. Change it deliberately, when you can watch it:
|
|
134
|
-
ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${drift.column}" TYPE ${drift.declared} USING "${drift.column}"::${drift.declared};`;
|
|
135
|
-
}
|
|
136
|
-
//#endregion
|
|
137
|
-
//#region src/schema/plan/diff-plan.ts
|
|
138
|
-
/** Postgres identifiers this module is willing to interpolate. */
|
|
139
|
-
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
140
|
-
/**
|
|
141
|
-
* Validation used to run only on the names read back OUT of the catalogue,
|
|
142
|
-
* which is the direction that cannot hurt anyone: those came from Postgres. The
|
|
143
|
-
* names going IN — the schema and table a collection declares — were quoted and
|
|
144
|
-
* concatenated on trust, and quoting is not escaping. A table named
|
|
145
|
-
* `x" (id int); DROP TABLE users; --` closes the quote and the statement, and
|
|
146
|
-
* the whole thing runs on the owner connection, which is the one connection in
|
|
147
|
-
* the system that is exempt from every RLS policy.
|
|
148
|
-
*
|
|
149
|
-
* That is only a config file on a self-hosted project, where the person writing
|
|
150
|
-
* it could run the SQL directly anyway. It is not only a config file where a
|
|
151
|
-
* collection can be defined over the wire — the live-schema and source editors
|
|
152
|
-
* both do that — and there the caller is an admin on the app, not an operator
|
|
153
|
-
* of the database.
|
|
154
|
-
*/
|
|
155
|
-
function assertSafeIdentifier(value, what) {
|
|
156
|
-
if (!SAFE_IDENTIFIER.test(value)) throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
159
|
-
/** Which pass a column belongs to. The order below is the order they run in. */
|
|
160
|
-
var isScalarProperty = (column) => (column.source.kind === "property" || column.source.kind === "implicit-id") && !column.primaryKey;
|
|
161
|
-
function diffPlanAgainstCatalogue(plan, existing, options = {}) {
|
|
162
|
-
const constraintPolicy = options.constraints ?? "additive";
|
|
163
|
-
const actions = [];
|
|
164
|
-
const withheldConstraints = [];
|
|
165
|
-
const columnTypeDrift = [];
|
|
166
|
-
const legacyForeignKeys = [];
|
|
167
|
-
const searchDrift = [];
|
|
168
|
-
const searchAdopted = [];
|
|
169
|
-
const collectionTables = plan.tables.filter((t) => t.kind === "collection");
|
|
170
|
-
const junctionTables = plan.tables.filter((t) => t.kind === "junction");
|
|
171
|
-
for (const enumPlan of plan.enums) {
|
|
172
|
-
assertSafeIdentifier(enumPlan.schema, "schema name");
|
|
173
|
-
if (existing.enums.has(enumPlan.qualified)) {
|
|
174
|
-
const current = existing.enumValues?.get(enumPlan.qualified);
|
|
175
|
-
if (!current) continue;
|
|
176
|
-
for (const value of enumPlan.labels) {
|
|
177
|
-
if (current.includes(value)) continue;
|
|
178
|
-
actions.push({
|
|
179
|
-
kind: "add-enum-value",
|
|
180
|
-
target: `${enumPlan.qualified}.${value}`,
|
|
181
|
-
sql: `ALTER TYPE "${enumPlan.schema}"."${enumPlan.name}" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
actions.push({
|
|
187
|
-
kind: "create-enum",
|
|
188
|
-
target: enumPlan.qualified,
|
|
189
|
-
sql: `CREATE TYPE "${enumPlan.schema}"."${enumPlan.name}" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(", ")});`
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
for (const statement of plan.extensions) actions.push({
|
|
193
|
-
kind: "create-extension",
|
|
194
|
-
target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""),
|
|
195
|
-
sql: statement
|
|
196
|
-
});
|
|
197
|
-
for (const statement of plan.functions) actions.push({
|
|
198
|
-
kind: "create-function",
|
|
199
|
-
target: functionTarget(statement),
|
|
200
|
-
sql: statement
|
|
201
|
-
});
|
|
202
|
-
const created = /* @__PURE__ */ new Set();
|
|
203
|
-
for (const table of collectionTables) {
|
|
204
|
-
if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;
|
|
205
|
-
created.add(table.qualified);
|
|
206
|
-
const schema = assertSafeIdentifier(table.schema, "schema name");
|
|
207
|
-
const name = assertSafeIdentifier(table.table, "table name");
|
|
208
|
-
const id = table.columns.find((c) => c.primaryKey);
|
|
209
|
-
const definition = id ? `"${assertSafeIdentifier(id.column, "column name")}" ${renderColumnDefinition(id)}` : "\"id\" TEXT PRIMARY KEY";
|
|
210
|
-
actions.push({
|
|
211
|
-
kind: "create-table",
|
|
212
|
-
target: table.qualified,
|
|
213
|
-
sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${name}" (${definition});`
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
for (const table of junctionTables) {
|
|
217
|
-
if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;
|
|
218
|
-
created.add(table.qualified);
|
|
219
|
-
const columns = table.columns.map((c) => `"${c.column}" ${renderColumnDefinition(c)}`).join(", ");
|
|
220
|
-
actions.push({
|
|
221
|
-
kind: "create-table",
|
|
222
|
-
target: table.qualified,
|
|
223
|
-
sql: `CREATE TABLE IF NOT EXISTS "${table.schema}"."${table.table}" (${columns}, PRIMARY KEY (${table.primaryKey.map((c) => `"${c}"`).join(", ")}));`
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
const addColumn = (table, column, definition) => {
|
|
227
|
-
if (existing.tables.get(table.qualified)?.has(column)) return;
|
|
228
|
-
assertSafeIdentifier(column, "column name");
|
|
229
|
-
actions.push({
|
|
230
|
-
kind: "add-column",
|
|
231
|
-
target: `${table.qualified}.${column}`,
|
|
232
|
-
sql: `ALTER TABLE "${table.schema}"."${table.table}" ADD COLUMN IF NOT EXISTS "${column}" ${definition};`
|
|
233
|
-
});
|
|
234
|
-
};
|
|
235
|
-
/**
|
|
236
|
-
* Move a column that is only missing because it was renamed.
|
|
237
|
-
*
|
|
238
|
-
* Returns true when it handled the column, so the caller skips the ordinary
|
|
239
|
-
* ADD. Adding would be the wrong move and a quiet one: the data is in the
|
|
240
|
-
* old column, `ADD COLUMN` creates the new one empty beside it, every
|
|
241
|
-
* statement succeeds, and the relation reads the empty one. A rename is
|
|
242
|
-
* metadata-only in Postgres, keeps the values, and carries the column's
|
|
243
|
-
* indexes and constraints with it.
|
|
244
|
-
*/
|
|
245
|
-
const renameLegacyColumn = (table, column) => {
|
|
246
|
-
const present = existing.tables.get(table.qualified);
|
|
247
|
-
if (!column.legacyColumn || !present) return false;
|
|
248
|
-
if (present.has(column.column) || !present.has(column.legacyColumn)) return false;
|
|
249
|
-
legacyForeignKeys.push({
|
|
250
|
-
table: table.qualified,
|
|
251
|
-
expected: column.column,
|
|
252
|
-
legacy: column.legacyColumn
|
|
253
|
-
});
|
|
254
|
-
actions.push({
|
|
255
|
-
kind: "rename-column",
|
|
256
|
-
target: `${table.qualified}.${column.column}`,
|
|
257
|
-
sql: `ALTER TABLE "${table.schema}"."${table.table}" RENAME COLUMN "${column.legacyColumn}" TO "${column.column}";`
|
|
258
|
-
});
|
|
259
|
-
return true;
|
|
260
|
-
};
|
|
261
|
-
/**
|
|
262
|
-
* A `NOT NULL` is safe exactly when it cannot fail against rows that are
|
|
263
|
-
* already there, and there are three ways to know that:
|
|
264
|
-
*
|
|
265
|
-
* - the table is being created by this plan (no rows yet);
|
|
266
|
-
* - the table exists and is empty;
|
|
267
|
-
* - the column arrives with a DEFAULT, which Postgres backfills into every
|
|
268
|
-
* existing row as part of ADD COLUMN.
|
|
269
|
-
*
|
|
270
|
-
* `populatedTables` absent means the caller does not know, and not knowing
|
|
271
|
-
* has to read as "assume rows" — the other direction emits a NOT NULL that
|
|
272
|
-
* is checked against live data and aborts the boot. Written as an explicit
|
|
273
|
-
* `!== undefined` because the optional-chain form quietly says *empty* when
|
|
274
|
-
* the fact is missing, which is the wrong way to be wrong.
|
|
275
|
-
*/
|
|
276
|
-
const tableIsEmpty = (table) => existing.populatedTables !== void 0 && existing.tables.has(table.qualified) && !existing.populatedTables.has(table.qualified);
|
|
277
|
-
/**
|
|
278
|
-
* The column is there and holds a different kind of value than the
|
|
279
|
-
* collection says it does. Reported, never altered: an unattended `ALTER
|
|
280
|
-
* COLUMN … TYPE` over customer data is not a thing to do quietly, and the
|
|
281
|
-
* *quiet* is what this fixes. A deploy that changed a `columnType` used to
|
|
282
|
-
* report success while the column stayed as it was, and the divergence
|
|
283
|
-
* surfaced later as every write failing. A column whose definition is owned
|
|
284
|
-
* elsewhere (auth, or a generated search column) is not compared: the
|
|
285
|
-
* declaration there is a rendered string rather than a type, and auth's own
|
|
286
|
-
* reconcile owns it.
|
|
287
|
-
*/
|
|
288
|
-
const reportTypeDrift = (table, column) => {
|
|
289
|
-
const key = `${table.qualified}.${column.column}`;
|
|
290
|
-
if (existing.tables.get(table.qualified)?.has(column.column) !== true || column.sqlDefinition) return;
|
|
291
|
-
const declaredType = renderPgType(column.type);
|
|
292
|
-
const actualType = existing.columnTypes?.get(key);
|
|
293
|
-
if (actualType && !typesAgree(declaredType, actualType)) columnTypeDrift.push({
|
|
294
|
-
table: table.qualified,
|
|
295
|
-
column: column.column,
|
|
296
|
-
declared: declaredType,
|
|
297
|
-
actual: actualType
|
|
298
|
-
});
|
|
299
|
-
};
|
|
300
|
-
/**
|
|
301
|
-
* Plan one column, with the constraints this database can safely take.
|
|
302
|
-
*
|
|
303
|
-
* The definition comes from the same renderer `schema.sql` uses, minus the
|
|
304
|
-
* constraints that are checked against live rows. That subtraction is the
|
|
305
|
-
* whole of what this path is allowed to decide for itself.
|
|
306
|
-
*/
|
|
307
|
-
const planColumn = (table, column) => {
|
|
308
|
-
const key = `${table.qualified}.${column.column}`;
|
|
309
|
-
const columnExists = existing.tables.get(table.qualified)?.has(column.column) === true;
|
|
310
|
-
const fresh = created.has(table.qualified);
|
|
311
|
-
const empty = tableIsEmpty(table);
|
|
312
|
-
const required = !column.nullable;
|
|
313
|
-
const hasDefault = column.default !== void 0;
|
|
314
|
-
reportTypeDrift(table, column);
|
|
315
|
-
const notNullIsSafe = fresh || empty || hasDefault;
|
|
316
|
-
const uniqueIsSafe = fresh || empty || !hasDefault;
|
|
317
|
-
const applicable = {
|
|
318
|
-
...column,
|
|
319
|
-
unique: column.unique && !columnExists && uniqueIsSafe,
|
|
320
|
-
nullable: column.nullable || !(notNullIsSafe || columnExists)
|
|
321
|
-
};
|
|
322
|
-
if (column.unique && !columnExists && !uniqueIsSafe) withheldConstraints.push({
|
|
323
|
-
target: key,
|
|
324
|
-
kind: "unique",
|
|
325
|
-
reason: `"${column.column}" is unique, but "${table.qualified}" ` + (existing.populatedTables === void 0 ? "may already hold rows" : "already holds rows") + " and the column's default would give every one of them the same value, so UNIQUE would fail as it was added.",
|
|
326
|
-
remedy: `Give each existing row its own value, then add the constraint — \`ALTER TABLE "${table.schema}"."${table.table}" ADD UNIQUE ("${column.column}")\` — or drop the property's default.`
|
|
327
|
-
});
|
|
328
|
-
if (required && !columnExists && !notNullIsSafe) withheldConstraints.push({
|
|
329
|
-
target: key,
|
|
330
|
-
kind: "not-null",
|
|
331
|
-
reason: column.source.kind === "relation" || column.source.kind === "reference" ? `"${column.column}" is a required link, but "${table.qualified}" already holds rows and the column has no default to backfill them with, so NOT NULL would be checked against data that does not have a value yet.` : `"${column.column}" is required, but "${table.qualified}" already holds rows and the column has no default to backfill them with, so NOT NULL would be checked against data that does not have a value yet.`,
|
|
332
|
-
remedy: column.source.kind === "relation" || column.source.kind === "reference" ? "Backfill the column, then add the constraint — or make the relation optional." : "Backfill the column, then add the constraint — or give the property a default so every existing row gets one."
|
|
333
|
-
});
|
|
334
|
-
addColumn(table, column.column, renderColumnDefinition(applicable));
|
|
335
|
-
if (!columnExists) return;
|
|
336
|
-
if (column.default && column.default.kind !== "identity" && !column.generated) {
|
|
337
|
-
const expression = column.default.kind === "sql" ? column.default.expression : column.default.sql;
|
|
338
|
-
if (!existing.columnDefaults?.has(key)) actions.push({
|
|
339
|
-
kind: "set-default",
|
|
340
|
-
target: key,
|
|
341
|
-
sql: `ALTER TABLE "${table.schema}"."${table.table}" ALTER COLUMN "${column.column}" SET DEFAULT ${expression};`
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
if (constraintPolicy !== "converge") return;
|
|
345
|
-
const isNotNull = existing.notNullColumns?.has(key) === true;
|
|
346
|
-
if (required && !isNotNull) if (empty) actions.push({
|
|
347
|
-
kind: "set-not-null",
|
|
348
|
-
target: key,
|
|
349
|
-
sql: `ALTER TABLE "${table.schema}"."${table.table}" ALTER COLUMN "${column.column}" SET NOT NULL;`
|
|
350
|
-
});
|
|
351
|
-
else withheldConstraints.push({
|
|
352
|
-
target: key,
|
|
353
|
-
kind: "not-null",
|
|
354
|
-
reason: `"${column.column}" became required, but "${table.qualified}" holds rows and any of them with no value would make SET NOT NULL fail.`,
|
|
355
|
-
remedy: "Backfill the column first — `UPDATE … SET \"" + column.column + "\" = … WHERE \"" + column.column + "\" IS NULL` — then apply this again."
|
|
356
|
-
});
|
|
357
|
-
if (!required && isNotNull) actions.push({
|
|
358
|
-
kind: "drop-not-null",
|
|
359
|
-
target: key,
|
|
360
|
-
sql: `ALTER TABLE "${table.schema}"."${table.table}" ALTER COLUMN "${column.column}" DROP NOT NULL;`
|
|
361
|
-
});
|
|
362
|
-
};
|
|
363
|
-
for (const table of collectionTables) {
|
|
364
|
-
assertSafeIdentifier(table.schema, "schema name");
|
|
365
|
-
assertSafeIdentifier(table.table, "table name");
|
|
366
|
-
for (const column of table.columns) {
|
|
367
|
-
if (!isScalarProperty(column)) continue;
|
|
368
|
-
planColumn(table, column);
|
|
369
|
-
}
|
|
370
|
-
for (const column of table.columns) {
|
|
371
|
-
if (column.source.kind !== "auth") continue;
|
|
372
|
-
addColumn(table, column.column, renderColumnDefinition(column));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
for (const table of collectionTables) {
|
|
376
|
-
if (!table.search) continue;
|
|
377
|
-
const definitions = new Map(table.columns.filter((c) => c.source.kind === "search").map((c) => [c.column, renderColumnDefinition(c)]));
|
|
378
|
-
for (const stamp of searchColumnStamps(table.search)) {
|
|
379
|
-
const definition = definitions.get(stamp.column);
|
|
380
|
-
const exists = existing.tables.get(table.qualified)?.has(stamp.column) === true;
|
|
381
|
-
const recorded = existing.columnComments?.get(`${table.qualified}.${stamp.column}`);
|
|
382
|
-
if (exists && recorded?.startsWith("rebase:search:v1:") && recorded !== stamp.fingerprint) {
|
|
383
|
-
searchDrift.push({
|
|
384
|
-
table: table.qualified,
|
|
385
|
-
column: stamp.column,
|
|
386
|
-
found: recorded,
|
|
387
|
-
expected: stamp.fingerprint,
|
|
388
|
-
rebuild: [
|
|
389
|
-
`ALTER TABLE "${table.schema}"."${table.table}" DROP COLUMN "${stamp.column}";`,
|
|
390
|
-
`ALTER TABLE "${table.schema}"."${table.table}" ADD COLUMN "${stamp.column}" ${definition};`,
|
|
391
|
-
stamp.sql
|
|
392
|
-
]
|
|
393
|
-
});
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
addColumn(table, stamp.column, definition);
|
|
397
|
-
if (exists && recorded === void 0) searchAdopted.push({
|
|
398
|
-
table: table.qualified,
|
|
399
|
-
column: stamp.column
|
|
400
|
-
});
|
|
401
|
-
if (recorded !== stamp.fingerprint) actions.push({
|
|
402
|
-
kind: "comment-column",
|
|
403
|
-
target: `${table.qualified}.${stamp.column}`,
|
|
404
|
-
sql: stamp.sql
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
for (const table of junctionTables) {
|
|
409
|
-
if (created.has(table.qualified)) continue;
|
|
410
|
-
for (const column of table.columns) {
|
|
411
|
-
if (renameLegacyColumn(table, column)) continue;
|
|
412
|
-
if (column.source.kind === "property") {
|
|
413
|
-
planColumn(table, column);
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
reportTypeDrift(table, column);
|
|
417
|
-
addColumn(table, column.column, renderPgType(column.type));
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
for (const table of collectionTables) for (const column of table.columns) {
|
|
421
|
-
if (column.source.kind !== "relation" && column.source.kind !== "reference") continue;
|
|
422
|
-
if (column.columnOwnedByProperty) continue;
|
|
423
|
-
if (renameLegacyColumn(table, column)) continue;
|
|
424
|
-
planColumn(table, column);
|
|
425
|
-
}
|
|
426
|
-
const knownConstraints = existing.constraints ?? /* @__PURE__ */ new Set();
|
|
427
|
-
const plannedConstraints = /* @__PURE__ */ new Set();
|
|
428
|
-
for (const table of [...collectionTables, ...junctionTables]) for (const column of table.columns) {
|
|
429
|
-
const fk = column.foreignKey;
|
|
430
|
-
if (!fk) continue;
|
|
431
|
-
const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;
|
|
432
|
-
if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;
|
|
433
|
-
plannedConstraints.add(name);
|
|
434
|
-
actions.push({
|
|
435
|
-
kind: "add-constraint",
|
|
436
|
-
target: name,
|
|
437
|
-
sql: `${fk.sql};`
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
const concurrent = (schema, table) => !created.has(`${schema}.${table}`);
|
|
441
|
-
for (const table of collectionTables) {
|
|
442
|
-
if (!table.search) continue;
|
|
443
|
-
for (const statement of searchIndexStatements(table.search)) actions.push({
|
|
444
|
-
kind: "create-index",
|
|
445
|
-
target: table.qualified,
|
|
446
|
-
sql: concurrent(table.schema, table.table) ? statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : statement
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
const vectorIndexSkipped = [];
|
|
450
|
-
for (const table of collectionTables) {
|
|
451
|
-
if (!table.vector) continue;
|
|
452
|
-
for (const spec of table.vector.specs) actions.push({
|
|
453
|
-
kind: "create-index",
|
|
454
|
-
target: `${spec.schema}.${spec.table}`,
|
|
455
|
-
sql: concurrent(spec.schema, spec.table) ? vectorIndexStatement(spec).replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : vectorIndexStatement(spec)
|
|
456
|
-
});
|
|
457
|
-
vectorIndexSkipped.push(...table.vector.skipped);
|
|
458
|
-
}
|
|
459
|
-
for (const spec of sortIndexSpecs(collectionTables.flatMap((t) => t.indexes))) actions.push({
|
|
460
|
-
kind: "create-index",
|
|
461
|
-
target: `${spec.schema}.${spec.table}`,
|
|
462
|
-
sql: collectionIndexStatement(spec, {
|
|
463
|
-
concurrently: concurrent(spec.schema, spec.table),
|
|
464
|
-
ifNotExists: true
|
|
465
|
-
})
|
|
466
|
-
});
|
|
467
|
-
for (const table of collectionTables) for (const trigger of table.triggers) {
|
|
468
|
-
if (existing.triggers?.has(`${table.qualified}.${trigger.name}`)) continue;
|
|
469
|
-
for (const statement of triggerStatements(trigger)) actions.push({
|
|
470
|
-
kind: "create-trigger",
|
|
471
|
-
target: `${table.qualified}.${trigger.name}`,
|
|
472
|
-
sql: statement
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
const orphanedRequiredColumns = [];
|
|
476
|
-
if (existing.notNullColumns && existing.columnDefaults) for (const table of collectionTables) {
|
|
477
|
-
const live = existing.tables.get(table.qualified);
|
|
478
|
-
if (!live || created.has(table.qualified)) continue;
|
|
479
|
-
const declared = /* @__PURE__ */ new Set();
|
|
480
|
-
for (const column of table.columns) {
|
|
481
|
-
declared.add(column.column);
|
|
482
|
-
if (column.legacyColumn) declared.add(column.legacyColumn);
|
|
483
|
-
}
|
|
484
|
-
for (const column of live) {
|
|
485
|
-
const key = `${table.qualified}.${column}`;
|
|
486
|
-
if (declared.has(column)) continue;
|
|
487
|
-
if (!existing.notNullColumns.has(key)) continue;
|
|
488
|
-
if (existing.columnDefaults.has(key)) continue;
|
|
489
|
-
orphanedRequiredColumns.push({
|
|
490
|
-
table: table.qualified,
|
|
491
|
-
column,
|
|
492
|
-
slug: table.slug
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
actions,
|
|
498
|
-
statements: actions.map((a) => a.sql),
|
|
499
|
-
legacyForeignKeys,
|
|
500
|
-
searchDrift,
|
|
501
|
-
searchAdopted,
|
|
502
|
-
vectorIndexSkipped,
|
|
503
|
-
withheldConstraints,
|
|
504
|
-
columnTypeDrift,
|
|
505
|
-
orphanedRequiredColumns
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
/** What a `CREATE FUNCTION` statement is called, for the action log. */
|
|
509
|
-
var functionTarget = (statement) => {
|
|
510
|
-
if (statement.includes("set_updated_at")) return SET_UPDATED_AT_FN;
|
|
511
|
-
return statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN;
|
|
512
|
-
};
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region src/schema/ensure-collection-tables.ts
|
|
515
|
-
/**
|
|
516
|
-
* Applying a schema plan to a database that is already running.
|
|
517
|
-
*
|
|
518
|
-
* ## What this module is
|
|
519
|
-
*
|
|
520
|
-
* A managed runtime boots someone else's compiled project against a database it
|
|
521
|
-
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
522
|
-
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
523
|
-
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
524
|
-
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
525
|
-
*
|
|
526
|
-
* Two halves, and only one of them is here. *What the collections ask for* is
|
|
527
|
-
* `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle
|
|
528
|
-
* file; *what the database already has, and which of those statements are safe
|
|
529
|
-
* to run against it* is `schema/plan/diff-plan.ts`. This module reads the
|
|
530
|
-
* catalogue, runs the statements, and reports what happened.
|
|
531
|
-
*
|
|
532
|
-
* ## Why additive-only, forever
|
|
533
|
-
*
|
|
534
|
-
* This runs unattended, against a database with customers' data in it, with no
|
|
535
|
-
* human reading a diff. So it may only ever do things that cannot lose data.
|
|
536
|
-
* See `diff-plan.ts` for the whole of that argument.
|
|
537
|
-
*/
|
|
538
|
-
var ensure_collection_tables_exports = /* @__PURE__ */ __exportAll({
|
|
539
|
-
describeDriverError: () => describeDriverError,
|
|
540
|
-
ensureCollectionTables: () => ensureCollectionTables,
|
|
541
|
-
planCollectionSchemaEnsure: () => planCollectionSchemaEnsure,
|
|
542
|
-
readExistingSchema: () => readExistingSchema,
|
|
543
|
-
readSchemaFactsFor: () => readSchemaFactsFor
|
|
544
|
-
});
|
|
545
|
-
/** The schema a collection lives in, validated before it reaches any DDL. */
|
|
546
|
-
function schemaOf(collection) {
|
|
547
|
-
return assertSafeIdentifier(collection.schema || "public", "schema name");
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Decide what to add. Pure — the caller supplies what exists and runs the
|
|
551
|
-
* result.
|
|
552
|
-
*
|
|
553
|
-
* A plan and a diff: the collections are read once, by `planSchema`, into the
|
|
554
|
-
* same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff
|
|
555
|
-
* subtracts what the database already has. Before that split this function had
|
|
556
|
-
* its own reading of every `Property`, and the audit found twelve places where
|
|
557
|
-
* it disagreed with the two generators — a required `author_id` that was
|
|
558
|
-
* NOT NULL after a push and nullable after a boot among them, on the one path
|
|
559
|
-
* with no developer in the loop.
|
|
560
|
-
*/
|
|
561
|
-
function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
|
|
562
|
-
return diffPlanAgainstCatalogue(planSchema(allCollections, { databaseExtensions: options.databaseExtensions }), existing, { constraints: options.constraints });
|
|
563
|
-
}
|
|
564
|
-
/** Read what the database has, for the schemas the collections live in. */
|
|
565
|
-
async function readExistingSchema(client, schemas) {
|
|
566
|
-
const tables = /* @__PURE__ */ new Map();
|
|
567
|
-
const enums = /* @__PURE__ */ new Set();
|
|
568
|
-
if (schemas.length === 0) return {
|
|
569
|
-
tables,
|
|
570
|
-
enums
|
|
571
|
-
};
|
|
572
|
-
const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
|
|
573
|
-
const notNullColumns = /* @__PURE__ */ new Set();
|
|
574
|
-
const columnTypes = /* @__PURE__ */ new Map();
|
|
575
|
-
const columnDefaults = /* @__PURE__ */ new Set();
|
|
576
|
-
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,
|
|
577
|
-
numeric_precision, numeric_scale
|
|
578
|
-
FROM information_schema.columns
|
|
579
|
-
WHERE table_schema IN (${inList})`);
|
|
580
|
-
for (const row of columns) {
|
|
581
|
-
const key = `${row.table_schema}.${row.table_name}`;
|
|
582
|
-
if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
|
|
583
|
-
tables.get(key).add(row.column_name);
|
|
584
|
-
if (row.is_nullable === "NO") notNullColumns.add(`${key}.${row.column_name}`);
|
|
585
|
-
if (row.udt_name) {
|
|
586
|
-
const modifier = row.udt_name === "numeric" && row.numeric_precision !== null ? `(${row.numeric_precision},${row.numeric_scale ?? 0})` : "";
|
|
587
|
-
columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);
|
|
588
|
-
}
|
|
589
|
-
if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);
|
|
590
|
-
}
|
|
591
|
-
const triggers = /* @__PURE__ */ new Set();
|
|
592
|
-
const { rows: triggerRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name
|
|
593
|
-
FROM pg_trigger t
|
|
594
|
-
JOIN pg_class c ON t.tgrelid = c.oid
|
|
595
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
596
|
-
WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`);
|
|
597
|
-
for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);
|
|
598
|
-
const populatedTables = /* @__PURE__ */ new Set();
|
|
599
|
-
const { rows: realTables } = await client.query(`SELECT n.nspname AS schema, c.relname AS name
|
|
600
|
-
FROM pg_class c
|
|
601
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
602
|
-
WHERE c.relkind IN ('r', 'p') AND n.nspname IN (${inList})`);
|
|
603
|
-
if (realTables.length > 0) {
|
|
604
|
-
const probes = realTables.map((row) => `SELECT ${quoteSqlLiteral(`${row.schema}.${row.name}`)} AS key, EXISTS(SELECT 1 FROM ${quoteRelation(row.schema, row.name)} LIMIT 1) AS populated`);
|
|
605
|
-
const { rows: populationRows } = await client.query(probes.join(" UNION ALL "));
|
|
606
|
-
for (const row of populationRows) if (row.populated) populatedTables.add(row.key);
|
|
607
|
-
}
|
|
608
|
-
const enumValues = /* @__PURE__ */ new Map();
|
|
609
|
-
const { rows: enumValueRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value
|
|
610
|
-
FROM pg_enum e
|
|
611
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
612
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
613
|
-
WHERE n.nspname IN (${inList})
|
|
614
|
-
ORDER BY t.typname, e.enumsortorder`);
|
|
615
|
-
for (const row of enumValueRows) {
|
|
616
|
-
const key = `${row.schema}.${row.name}`;
|
|
617
|
-
if (!enumValues.has(key)) enumValues.set(key, []);
|
|
618
|
-
enumValues.get(key).push(row.value);
|
|
619
|
-
}
|
|
620
|
-
const { rows: enumRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name
|
|
621
|
-
FROM pg_type t
|
|
622
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
623
|
-
WHERE t.typtype = 'e' AND n.nspname IN (${inList})`);
|
|
624
|
-
for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
|
|
625
|
-
const constraints = /* @__PURE__ */ new Set();
|
|
626
|
-
const { rows: constraintRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, con.conname AS name
|
|
627
|
-
FROM pg_constraint con
|
|
628
|
-
JOIN pg_class c ON con.conrelid = c.oid
|
|
629
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
630
|
-
WHERE n.nspname IN (${inList})`);
|
|
631
|
-
for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
|
|
632
|
-
const columnComments = /* @__PURE__ */ new Map();
|
|
633
|
-
const { rows: commentRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment
|
|
634
|
-
FROM pg_description d
|
|
635
|
-
JOIN pg_class c ON d.objoid = c.oid
|
|
636
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
637
|
-
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid
|
|
638
|
-
WHERE d.objsubid > 0 AND n.nspname IN (${inList})`);
|
|
639
|
-
for (const row of commentRows) {
|
|
640
|
-
if (row.comment == null) continue;
|
|
641
|
-
columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);
|
|
642
|
-
}
|
|
643
|
-
return {
|
|
644
|
-
tables,
|
|
645
|
-
enums,
|
|
646
|
-
constraints,
|
|
647
|
-
columnComments,
|
|
648
|
-
enumValues,
|
|
649
|
-
notNullColumns,
|
|
650
|
-
populatedTables,
|
|
651
|
-
columnTypes,
|
|
652
|
-
columnDefaults,
|
|
653
|
-
triggers
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* What to tell an operator whose `search` block no longer matches its column.
|
|
658
|
-
*
|
|
659
|
-
* Every line here is doing work: naming the collection is not enough, because
|
|
660
|
-
* the symptom (a search that finds nothing) points at the data, not the schema;
|
|
661
|
-
* and the remediation has to be exact, because it is a table rewrite the
|
|
662
|
-
* operator is being asked to schedule rather than discover.
|
|
663
|
-
*/
|
|
664
|
-
function searchDriftMessage(drift) {
|
|
665
|
-
return "The `search` block changed after its generated column was created, and Postgres cannot alter a generated expression in place.\nRebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path may not schedule on your behalf.\nUntil it is rebuilt the column keeps indexing the previous fields, weights and language — searches for anything added since return nothing, which reads from outside as \"no such row\".\nRun these (or revert the block to what the column was built from), then boot again:\n" + drift.map((d) => ` "${d.table}"."${d.column}" was generated from a different \`search\` block (recorded ${d.found}, current ${d.expected}).\n` + d.rebuild.map((s) => ` ${s}`).join("\n")).join("\n") + "\n The GIN index is dropped with the column and recreated concurrently on the next boot.";
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Read what the database looks like, for the schemas a set of collections
|
|
669
|
-
* lives in.
|
|
670
|
-
*
|
|
671
|
-
* The same read `ensureCollectionTables` does at boot, exposed on its own for
|
|
672
|
-
* the callers that want to *plan* against a real database without changing it —
|
|
673
|
-
* the live schema editor, which has to tell somebody what a change would do
|
|
674
|
-
* before they agree to it.
|
|
675
|
-
*/
|
|
676
|
-
async function readSchemaFactsFor(client, collections) {
|
|
677
|
-
const relational = relationalCollections(collections);
|
|
678
|
-
return readExistingSchema(client, Array.from(/* @__PURE__ */ new Set([...relational.map(schemaOf), ...planJunctionTables(relational).map((junction) => junction.schema)])));
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Bring the database up to date. Returns what it did.
|
|
682
|
-
*
|
|
683
|
-
* Each statement runs on its own rather than in one transaction: they are all
|
|
684
|
-
* independently safe and idempotent, and a single failure (an enum label that
|
|
685
|
-
* cannot be added, say) should not roll back the tables that were created fine.
|
|
686
|
-
* The error is surfaced with the statement that caused it.
|
|
687
|
-
*/
|
|
688
|
-
async function ensureCollectionTables(client, collections, log, options = {}) {
|
|
689
|
-
const schema = planSchema(collections, { databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions() });
|
|
690
|
-
const schemas = Array.from(new Set(schema.tables.map((table) => table.schema)));
|
|
691
|
-
for (const name of schemas) {
|
|
692
|
-
assertSafeIdentifier(name, "schema name");
|
|
693
|
-
if (name !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${name}";`);
|
|
694
|
-
}
|
|
695
|
-
if (schema.tables.some((table) => table.triggers.length > 0)) await client.query(`CREATE SCHEMA IF NOT EXISTS "${REBASE_SCHEMA}";`);
|
|
696
|
-
const plan = diffPlanAgainstCatalogue(schema, await readExistingSchema(client, schemas), { constraints: options.constraints });
|
|
697
|
-
const failures = [];
|
|
698
|
-
for (const legacy of plan.legacyForeignKeys) {
|
|
699
|
-
const message = `Renaming "${legacy.table}"."${legacy.legacy}" to "${legacy.expected}". The old name is the one Rebase derived for this relation before it singularized properly; the column keeps its data, indexes and constraints. To keep the old name instead, set \`localKey: "${legacy.legacy}"\` on the relation and this will stop.`;
|
|
700
|
-
logger.info(`[schema] ${message}`);
|
|
701
|
-
log?.(message);
|
|
702
|
-
}
|
|
703
|
-
if (plan.searchDrift.length > 0) throw new Error(searchDriftMessage(plan.searchDrift));
|
|
704
|
-
for (const adopted of plan.searchAdopted) {
|
|
705
|
-
const message = `Adopting the existing generated column "${adopted.table}"."${adopted.column}" and recording what the current \`search\` block would generate. Any later change to that block will be detected and refused; a change made *before* this version was deployed cannot be, so if search has been missing content, rebuild the column once: ALTER TABLE "${adopted.table.split(".").join("\".\"")}" DROP COLUMN "${adopted.column}"; and boot again.`;
|
|
706
|
-
logger.info(`[schema] ${message}`);
|
|
707
|
-
log?.(message);
|
|
708
|
-
}
|
|
709
|
-
for (const skip of plan.vectorIndexSkipped) {
|
|
710
|
-
const message = `No ANN index on "${skip.table}"."${skip.column}": ${skip.reason}`;
|
|
711
|
-
logger.warn(`[schema] ${message}`);
|
|
712
|
-
log?.(message);
|
|
713
|
-
}
|
|
714
|
-
for (const withheld of plan.withheldConstraints) {
|
|
715
|
-
const message = `No ${withheld.kind === "unique" ? "UNIQUE" : "NOT NULL"} on "${withheld.target}": ${withheld.reason} ${withheld.remedy}`;
|
|
716
|
-
logger.warn(`[schema] ${message}`);
|
|
717
|
-
log?.(message);
|
|
718
|
-
}
|
|
719
|
-
for (const drift of plan.columnTypeDrift) {
|
|
720
|
-
const message = columnTypeDriftMessage(drift);
|
|
721
|
-
logger.warn(`[schema] ${message}`);
|
|
722
|
-
log?.(message);
|
|
723
|
-
}
|
|
724
|
-
for (const orphan of plan.orphanedRequiredColumns) {
|
|
725
|
-
const [schema, table] = orphan.table.split(".");
|
|
726
|
-
const message = `"${orphan.table}"."${orphan.column}" is NOT NULL with no default, and the "${orphan.slug}" collection no longer declares it. Every insert will be rejected for a field you cannot set (PG_23502, naming "${orphan.column}"). This is what a renamed property looks like: the new column was added, and the old one cannot be dropped unattended. Fix it with ONE of:
|
|
727
|
-
ALTER TABLE "${schema}"."${table}" DROP COLUMN "${orphan.column}"; -- the rename is complete\n ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${orphan.column}" DROP NOT NULL; -- keep the data, stop requiring it\n ...or declare the property again, if the column is still wanted.`;
|
|
728
|
-
logger.warn(`[schema] ${message}`);
|
|
729
|
-
log?.(message);
|
|
730
|
-
}
|
|
731
|
-
if (plan.actions.length === 0) {
|
|
732
|
-
log?.("Schema is up to date; nothing to create.");
|
|
733
|
-
return {
|
|
734
|
-
...plan,
|
|
735
|
-
failures
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
for (const action of plan.actions) try {
|
|
739
|
-
if (await applyAction(client, action)) log?.(`${action.kind}: ${action.target}`);
|
|
740
|
-
else log?.(`${action.kind}: ${action.target} (already created by a peer)`);
|
|
741
|
-
} catch (err) {
|
|
742
|
-
const message = describeDriverError(err);
|
|
743
|
-
if (action.kind === "add-constraint" || action.kind === "comment-column" || action.kind === "create-index" && !/CREATE\s+UNIQUE\s+INDEX/i.test(action.sql)) {
|
|
744
|
-
failures.push({
|
|
745
|
-
kind: action.kind,
|
|
746
|
-
target: action.target,
|
|
747
|
-
error: message
|
|
748
|
-
});
|
|
749
|
-
continue;
|
|
750
|
-
}
|
|
751
|
-
throw new Error(`Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\n ${action.sql}`);
|
|
752
|
-
}
|
|
753
|
-
return {
|
|
754
|
-
...plan,
|
|
755
|
-
failures
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* What actually went wrong, rather than what the ORM said about it.
|
|
760
|
-
*
|
|
761
|
-
* Drizzle wraps every driver error, and its `message` is literally
|
|
762
|
-
* ``Failed query: ${sql}\nparams: ${params}`` — the statement we already know,
|
|
763
|
-
* and not one word of Postgres's answer. Reporting `err.message` therefore
|
|
764
|
-
* produced a failure that named the statement twice and the *reason* zero
|
|
765
|
-
* times, so four unrelated boot failures — a missing extension, a wrong
|
|
766
|
-
* privilege, a bad index method, a constraint the rows violate — all read
|
|
767
|
-
* identically to whoever had to fix them.
|
|
768
|
-
*
|
|
769
|
-
* Two consequences, both observed in the field before this existed:
|
|
770
|
-
*
|
|
771
|
-
* 1. On a managed deployment the reason never reaches anybody. Boot dies, the
|
|
772
|
-
* pod never goes ready, and the deploy reports a readiness timeout — so the
|
|
773
|
-
* one line that says why is the one line that was thrown away here.
|
|
774
|
-
* 2. {@link vectorExtensionHint} matches on the error text. Against the
|
|
775
|
-
* wrapper it matched nothing, so the hint written specifically to explain a
|
|
776
|
-
* missing pgvector never fired on the path that raises it.
|
|
777
|
-
*
|
|
778
|
-
* `detail` and `hint` come along because they are the fields Postgres uses to
|
|
779
|
-
* say which object it meant and what to do about it — `CREATE EXTENSION` names
|
|
780
|
-
* the missing library there, and a rejected constraint names the row. This
|
|
781
|
-
* writes into the tenant's own boot log, about the tenant's own database.
|
|
782
|
-
*/
|
|
783
|
-
function describeDriverError(err) {
|
|
784
|
-
const pg = extractPgError(err);
|
|
785
|
-
if (pg) {
|
|
786
|
-
const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];
|
|
787
|
-
if (pg.detail) parts.push(` detail: ${pg.detail}`);
|
|
788
|
-
if (pg.hint) parts.push(` hint: ${pg.hint}`);
|
|
789
|
-
return parts.join("\n");
|
|
790
|
-
}
|
|
791
|
-
const cause = extractCauseMessage(err);
|
|
792
|
-
if (cause) return cause;
|
|
793
|
-
return err instanceof Error ? err.message : String(err);
|
|
794
|
-
}
|
|
795
|
-
/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */
|
|
796
|
-
var CONCURRENTLY_RE = /\sCONCURRENTLY\s/i;
|
|
797
|
-
/**
|
|
798
|
-
* `25001 active_sql_transaction` — "cannot run inside a transaction block".
|
|
799
|
-
*
|
|
800
|
-
* Only ever raised here by `CREATE INDEX CONCURRENTLY`, and only when the
|
|
801
|
-
* connection we were handed is inside a transaction. Deliberately narrow: it
|
|
802
|
-
* decides to retry a *different* statement, so it must not fire on anything it
|
|
803
|
-
* has not been reasoned about.
|
|
804
|
-
*/
|
|
805
|
-
function isTransactionBlockRefusal(err) {
|
|
806
|
-
return extractPgError(err)?.code === "25001";
|
|
807
|
-
}
|
|
808
|
-
/** Attempts per action, including the first. Matches the server's bootstraps. */
|
|
809
|
-
var DDL_ATTEMPTS = 4;
|
|
810
|
-
/**
|
|
811
|
-
* Run one planned statement, surviving a simultaneous boot.
|
|
812
|
-
*
|
|
813
|
-
* Every statement in a plan is written to be idempotent, and that is not the
|
|
814
|
-
* same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the
|
|
815
|
-
* catalog and then writes to it as two steps, so peers starting together both
|
|
816
|
-
* see "absent" and the loser gets a duplicate key on a *catalog* index. Measured
|
|
817
|
-
* against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is
|
|
818
|
-
* worse, because Postgres has no `IF NOT EXISTS` for it at all.
|
|
819
|
-
*
|
|
820
|
-
* What made that fatal here rather than merely noisy is the loop this sits in.
|
|
821
|
-
* A losing statement threw, and the throw abandoned **every remaining action in
|
|
822
|
-
* the plan** — so a replica that lost one race came up missing tables it never
|
|
823
|
-
* attempted, and the boot log blamed the one statement that failed.
|
|
824
|
-
*
|
|
825
|
-
* @returns `true` if this process applied the statement, `false` if a peer had
|
|
826
|
-
* already created the object. The distinction is only for the log; both mean
|
|
827
|
-
* the object is now there.
|
|
828
|
-
* @throws the original error for anything that is not a race — a syntax error, a
|
|
829
|
-
* permission failure, a unique constraint the customer's own rows violate.
|
|
830
|
-
*/
|
|
831
|
-
async function applyAction(client, action) {
|
|
832
|
-
let sqlText = action.sql;
|
|
833
|
-
for (let attempt = 1;; attempt++) try {
|
|
834
|
-
await client.query(sqlText);
|
|
835
|
-
return true;
|
|
836
|
-
} catch (err) {
|
|
837
|
-
if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {
|
|
838
|
-
sqlText = sqlText.replace(CONCURRENTLY_RE, " ");
|
|
839
|
-
logger.warn(`[schema] ${action.kind} ${action.target}: this connection is inside a transaction, so the index is being built with a write lock rather than concurrently. Writes to this table block until it finishes.`);
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
if (isDuplicateObjectRace(err)) {
|
|
843
|
-
logger.debug(`[schema] ${action.kind} ${action.target}: already created by another instance`);
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {
|
|
847
|
-
logger.debug(`[schema] ${action.kind} ${action.target}: lost a race with another instance (attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`);
|
|
848
|
-
await new Promise((resolve) => setTimeout(resolve, 40 * attempt * (1 + Math.random())));
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
throw err;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
//#endregion
|
|
855
|
-
export { readSchemaFactsFor as a, readExistingSchema as i, ensure_collection_tables_exports as n, planCollectionSchemaEnsure as r, describeDriverError as t };
|
|
856
|
-
|
|
857
|
-
//# sourceMappingURL=ensure-collection-tables-BEEjn5cn.js.map
|