@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g08eed46
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-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
- package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +81 -7
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
- package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
- package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
- package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
- package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
- package/dist/cli-errors-C3g_kHBw.js.map +1 -0
- package/dist/cli-errors.d.ts +19 -0
- package/dist/cli-helpers.d.ts +47 -0
- package/dist/cli.js +233 -19
- package/dist/cli.js.map +1 -1
- package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
- package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
- package/dist/collections/validate-relations.d.ts +7 -0
- package/dist/column-plan-helpers-CpILzHJS.js +145 -0
- package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
- package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
- package/dist/doctor-D5SrGJ5P.js.map +1 -0
- package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
- package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
- package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
- package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
- package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
- package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
- package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
- package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
- package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
- package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +23 -4
- package/dist/index.es.js +1991 -580
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
- package/dist/plan-schema-Hgl62S-w.js +1531 -0
- package/dist/plan-schema-Hgl62S-w.js.map +1 -0
- package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
- package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
- package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
- package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
- package/dist/schema/catalogue-schema.d.ts +97 -0
- package/dist/schema/collection-index.d.ts +9 -0
- package/dist/schema/column-plan-helpers.d.ts +125 -0
- package/dist/schema/config-relations.d.ts +32 -0
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +9 -0
- package/dist/schema/dynamic-tables.d.ts +27 -6
- package/dist/schema/ensure-collection-tables.d.ts +31 -268
- package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
- package/dist/schema/generate-postgres-ddl.js +8 -2
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generated-column-conflicts.d.ts +84 -0
- package/dist/schema/generated-schema-diff.d.ts +56 -0
- package/dist/schema/generated-schema-staleness.d.ts +33 -0
- package/dist/schema/introspect-db-logic.d.ts +2 -0
- package/dist/schema/plan/diff-plan.d.ts +220 -0
- package/dist/schema/plan/plan-schema.d.ts +84 -0
- package/dist/schema/plan/render-ddl.d.ts +62 -0
- package/dist/schema/plan/render-drizzle.d.ts +31 -0
- package/dist/schema/plan/types.d.ts +410 -0
- package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
- package/dist/schema/relation-names.d.ts +28 -0
- package/dist/schema/search-column.d.ts +9 -0
- package/dist/security/rls-enforcement.d.ts +17 -0
- package/dist/services/FetchService.d.ts +176 -36
- package/dist/services/PersistService.d.ts +17 -4
- package/dist/services/RelationService.d.ts +11 -3
- package/dist/services/collection-helpers.d.ts +16 -2
- package/dist/services/dataService.d.ts +29 -2
- package/dist/services/field-op-sql.d.ts +16 -0
- package/dist/services/field-viewer.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +35 -1
- package/dist/services/row-pipeline.d.ts +29 -5
- package/dist/services/soft-delete.d.ts +73 -0
- package/dist/utils/pg-error-utils.d.ts +41 -1
- package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
- package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
- package/package.json +18 -10
- package/dist/cli-errors-DxN4yzdH.js.map +0 -1
- package/dist/doctor-D7HWmGzs.js.map +0 -1
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
- package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { c as
|
|
8
|
-
import {
|
|
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-Hgl62S-w.js";
|
|
5
|
+
import { d as extractPgError, u as extractCauseMessage } from "./cli-errors-C3g_kHBw.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-CR2xcS7e.js";
|
|
9
9
|
import { isConcurrentDdlRace, isDuplicateObjectRace, logger } from "@rebasepro/server";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { REBASE_SCHEMA, declaredDatabaseExtensions } from "@rebasepro/types";
|
|
11
|
+
import { relationalCollections } from "@rebasepro/common";
|
|
12
12
|
//#region src/schema/column-type-drift.ts
|
|
13
13
|
/**
|
|
14
14
|
* Types that mean the same thing to everything above the driver.
|
|
@@ -99,7 +99,16 @@ function typesAgree(declared, actual) {
|
|
|
99
99
|
const a = typeFamily(declared);
|
|
100
100
|
const b = typeFamily(actual);
|
|
101
101
|
if (a === null || b === null) return true;
|
|
102
|
-
|
|
102
|
+
if (a !== b) return false;
|
|
103
|
+
if (a !== "numeric") return true;
|
|
104
|
+
const declaredModifier = numericModifier(declared);
|
|
105
|
+
if (declaredModifier === null) return true;
|
|
106
|
+
return declaredModifier === numericModifier(actual);
|
|
107
|
+
}
|
|
108
|
+
/** `numeric(10,2)` / `NUMERIC(10, 2)` → `10,2`; a bare `numeric` → `null`. */
|
|
109
|
+
function numericModifier(raw) {
|
|
110
|
+
const match = raw.match(/\(([^)]*)\)/);
|
|
111
|
+
return match ? match[1].replace(/\s+/g, "") : null;
|
|
103
112
|
}
|
|
104
113
|
/**
|
|
105
114
|
* The sentence a boot log says about one drifted column.
|
|
@@ -114,51 +123,10 @@ function columnTypeDriftMessage(drift) {
|
|
|
114
123
|
ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${drift.column}" TYPE ${drift.declared} USING "${drift.column}"::${drift.declared};`;
|
|
115
124
|
}
|
|
116
125
|
//#endregion
|
|
117
|
-
//#region src/schema/
|
|
118
|
-
/**
|
|
119
|
-
* Bringing a database up to date with a bundle's collections, additively.
|
|
120
|
-
*
|
|
121
|
-
* ## Why this exists
|
|
122
|
-
*
|
|
123
|
-
* A managed runtime boots someone else's compiled project against a database it
|
|
124
|
-
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
125
|
-
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
126
|
-
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
127
|
-
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
128
|
-
*
|
|
129
|
-
* ## Why additive-only, forever
|
|
130
|
-
*
|
|
131
|
-
* This runs unattended, against a database with customers' data in it, with no
|
|
132
|
-
* human reading a diff. So it may only ever do things that cannot lose data:
|
|
133
|
-
* create a missing table, add a missing column, create a missing enum type.
|
|
134
|
-
*
|
|
135
|
-
* It will **never** drop a table or a column, narrow a type, or alter a
|
|
136
|
-
* constraint. A removed field leaves its column behind; a renamed field looks
|
|
137
|
-
* like an addition and the old column stays. That is the correct trade for an
|
|
138
|
-
* automated path — the alternative is an unattended process that can silently
|
|
139
|
-
* destroy a column, which is precisely the failure `db push` was hardened
|
|
140
|
-
* against. Destructive changes stay a deliberate, human-reviewed migration.
|
|
141
|
-
*
|
|
142
|
-
* Because of that, this is safe to run on every boot, and re-running it is a
|
|
143
|
-
* no-op.
|
|
144
|
-
*/
|
|
145
|
-
var ensure_collection_tables_exports = /* @__PURE__ */ __exportAll({
|
|
146
|
-
describeDriverError: () => describeDriverError,
|
|
147
|
-
ensureCollectionTables: () => ensureCollectionTables,
|
|
148
|
-
planCollectionSchemaEnsure: () => planCollectionSchemaEnsure,
|
|
149
|
-
readExistingSchema: () => readExistingSchema,
|
|
150
|
-
readSchemaFactsFor: () => readSchemaFactsFor
|
|
151
|
-
});
|
|
126
|
+
//#region src/schema/plan/diff-plan.ts
|
|
152
127
|
/** Postgres identifiers this module is willing to interpolate. */
|
|
153
128
|
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
154
|
-
function assertSafeIdentifier(value, what) {
|
|
155
|
-
if (!SAFE_IDENTIFIER.test(value)) throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);
|
|
156
|
-
return value;
|
|
157
|
-
}
|
|
158
129
|
/**
|
|
159
|
-
* The schema a collection lives in — validated, because it is about to be
|
|
160
|
-
* interpolated into DDL.
|
|
161
|
-
*
|
|
162
130
|
* Validation used to run only on the names read back OUT of the catalogue,
|
|
163
131
|
* which is the direction that cannot hurt anyone: those came from Postgres. The
|
|
164
132
|
* names going IN — the schema and table a collection declares — were quoted and
|
|
@@ -173,349 +141,320 @@ function assertSafeIdentifier(value, what) {
|
|
|
173
141
|
* both do that — and there the caller is an admin on the app, not an operator
|
|
174
142
|
* of the database.
|
|
175
143
|
*/
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
/** A collection's table name, validated for the same reason as {@link schemaOf}. */
|
|
180
|
-
function tableOf(collection) {
|
|
181
|
-
return assertSafeIdentifier(getTableName(collection), "table name");
|
|
182
|
-
}
|
|
183
|
-
function qualified(collection) {
|
|
184
|
-
return `${schemaOf(collection)}.${tableOf(collection)}`;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Enum types a collection's properties require, as `schema.typename`.
|
|
188
|
-
*
|
|
189
|
-
* Named exactly as the DDL generator names them (`<table>_<column>`), because
|
|
190
|
-
* a column added here has to reference the same type the generator would have
|
|
191
|
-
* created — a second, differently-named type for the same field would be a
|
|
192
|
-
* silent schema fork.
|
|
193
|
-
*/
|
|
194
|
-
function requiredEnums(collection) {
|
|
195
|
-
const table = tableOf(collection);
|
|
196
|
-
const schema = schemaOf(collection);
|
|
197
|
-
const out = [];
|
|
198
|
-
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
199
|
-
const p = prop;
|
|
200
|
-
if (!("enum" in p) || !p.enum) continue;
|
|
201
|
-
if (p.type !== "string" && p.type !== "number") continue;
|
|
202
|
-
const values = p.enum.map((entry) => entry && typeof entry === "object" && "id" in entry ? String(entry.id) : String(entry)).filter((v) => v.length > 0);
|
|
203
|
-
if (values.length === 0) continue;
|
|
204
|
-
out.push({
|
|
205
|
-
name: `${schema}.${table}_${resolveColumnName(propName, p)}`,
|
|
206
|
-
values
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
return out;
|
|
144
|
+
function assertSafeIdentifier(value, what) {
|
|
145
|
+
if (!SAFE_IDENTIFIER.test(value)) throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);
|
|
146
|
+
return value;
|
|
210
147
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
* Ordering matters and is deliberate: enum types before the tables and columns
|
|
215
|
-
* that reference them, tables before the columns added to other tables (a new
|
|
216
|
-
* table may be the target of a relation), and nothing is emitted twice.
|
|
217
|
-
*/
|
|
218
|
-
function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
|
|
148
|
+
/** Which pass a column belongs to. The order below is the order they run in. */
|
|
149
|
+
var isScalarProperty = (column) => (column.source.kind === "property" || column.source.kind === "implicit-id") && !column.primaryKey;
|
|
150
|
+
function diffPlanAgainstCatalogue(plan, existing, options = {}) {
|
|
219
151
|
const constraintPolicy = options.constraints ?? "additive";
|
|
152
|
+
const actions = [];
|
|
220
153
|
const withheldConstraints = [];
|
|
221
154
|
const columnTypeDrift = [];
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
155
|
+
const legacyForeignKeys = [];
|
|
156
|
+
const searchDrift = [];
|
|
157
|
+
const searchAdopted = [];
|
|
158
|
+
const collectionTables = plan.tables.filter((t) => t.kind === "collection");
|
|
159
|
+
const junctionTables = plan.tables.filter((t) => t.kind === "junction");
|
|
160
|
+
for (const enumPlan of plan.enums) {
|
|
161
|
+
assertSafeIdentifier(enumPlan.schema, "schema name");
|
|
162
|
+
if (existing.enums.has(enumPlan.qualified)) {
|
|
163
|
+
const current = existing.enumValues?.get(enumPlan.qualified);
|
|
164
|
+
if (!current) continue;
|
|
165
|
+
for (const value of enumPlan.labels) {
|
|
232
166
|
if (current.includes(value)) continue;
|
|
233
167
|
actions.push({
|
|
234
168
|
kind: "add-enum-value",
|
|
235
|
-
target: `${
|
|
236
|
-
sql: `ALTER TYPE "${schema}"."${
|
|
169
|
+
target: `${enumPlan.qualified}.${value}`,
|
|
170
|
+
sql: `ALTER TYPE "${enumPlan.schema}"."${enumPlan.name}" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`
|
|
237
171
|
});
|
|
238
172
|
}
|
|
239
173
|
continue;
|
|
240
174
|
}
|
|
241
|
-
plannedEnums.add(name);
|
|
242
|
-
const [schema, typeName] = name.split(".");
|
|
243
175
|
actions.push({
|
|
244
176
|
kind: "create-enum",
|
|
245
|
-
target:
|
|
246
|
-
sql: `CREATE TYPE "${schema}"."${
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
const searchSpecs = collections.map((c) => buildSearchColumnSpec(c)).filter((spec) => spec !== void 0);
|
|
250
|
-
const plannedExtensions = /* @__PURE__ */ new Set();
|
|
251
|
-
const planExtension = (statement) => {
|
|
252
|
-
if (plannedExtensions.has(statement)) return;
|
|
253
|
-
plannedExtensions.add(statement);
|
|
254
|
-
actions.push({
|
|
255
|
-
kind: "create-extension",
|
|
256
|
-
target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""),
|
|
257
|
-
sql: statement
|
|
258
|
-
});
|
|
259
|
-
};
|
|
260
|
-
for (const spec of searchSpecs) for (const statement of searchExtensionStatements(spec)) planExtension(statement);
|
|
261
|
-
if (vectorExtensionDeclared(options.databaseExtensions) && collections.some((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0)) planExtension(vectorExtensionStatement());
|
|
262
|
-
const plannedFunctions = /* @__PURE__ */ new Set();
|
|
263
|
-
for (const spec of searchSpecs) for (const statement of searchHelperFunctions(spec)) {
|
|
264
|
-
if (plannedFunctions.has(statement)) continue;
|
|
265
|
-
plannedFunctions.add(statement);
|
|
266
|
-
actions.push({
|
|
267
|
-
kind: "create-function",
|
|
268
|
-
target: statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN,
|
|
269
|
-
sql: statement
|
|
177
|
+
target: enumPlan.qualified,
|
|
178
|
+
sql: `CREATE TYPE "${enumPlan.schema}"."${enumPlan.name}" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(", ")});`
|
|
270
179
|
});
|
|
271
180
|
}
|
|
181
|
+
for (const statement of plan.extensions) actions.push({
|
|
182
|
+
kind: "create-extension",
|
|
183
|
+
target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""),
|
|
184
|
+
sql: statement
|
|
185
|
+
});
|
|
186
|
+
for (const statement of plan.functions) actions.push({
|
|
187
|
+
kind: "create-function",
|
|
188
|
+
target: functionTarget(statement),
|
|
189
|
+
sql: statement
|
|
190
|
+
});
|
|
272
191
|
const created = /* @__PURE__ */ new Set();
|
|
273
|
-
for (const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1]) : "id";
|
|
281
|
-
const idProp = idEntry?.[1];
|
|
282
|
-
const idType = idProp ? getSqlColumnType(idEntry[0], idProp, collection, collections) : "TEXT";
|
|
283
|
-
let idDef = `"${assertSafeIdentifier(idName, "column name")}" ${idType} PRIMARY KEY`;
|
|
284
|
-
if (idProp?.type === "string" && idProp.isId === "uuid") idDef += " DEFAULT gen_random_uuid()";
|
|
192
|
+
for (const table of collectionTables) {
|
|
193
|
+
if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;
|
|
194
|
+
created.add(table.qualified);
|
|
195
|
+
const schema = assertSafeIdentifier(table.schema, "schema name");
|
|
196
|
+
const name = assertSafeIdentifier(table.table, "table name");
|
|
197
|
+
const id = table.columns.find((c) => c.primaryKey);
|
|
198
|
+
const definition = id ? `"${assertSafeIdentifier(id.column, "column name")}" ${renderColumnDefinition(id)}` : "\"id\" TEXT PRIMARY KEY";
|
|
285
199
|
actions.push({
|
|
286
200
|
kind: "create-table",
|
|
287
|
-
target:
|
|
288
|
-
sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${
|
|
201
|
+
target: table.qualified,
|
|
202
|
+
sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${name}" (${definition});`
|
|
289
203
|
});
|
|
290
204
|
}
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
created.add(key);
|
|
205
|
+
for (const table of junctionTables) {
|
|
206
|
+
if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;
|
|
207
|
+
created.add(table.qualified);
|
|
208
|
+
const columns = table.columns.map((c) => `"${c.column}" ${renderColumnDefinition(c)}`).join(", ");
|
|
296
209
|
actions.push({
|
|
297
210
|
kind: "create-table",
|
|
298
|
-
target:
|
|
299
|
-
sql:
|
|
211
|
+
target: table.qualified,
|
|
212
|
+
sql: `CREATE TABLE IF NOT EXISTS "${table.schema}"."${table.table}" (${columns}, PRIMARY KEY (${table.primaryKey.map((c) => `"${c}"`).join(", ")}));`
|
|
300
213
|
});
|
|
301
214
|
}
|
|
302
|
-
const
|
|
215
|
+
const addColumn = (table, column, definition) => {
|
|
216
|
+
if (existing.tables.get(table.qualified)?.has(column)) return;
|
|
217
|
+
assertSafeIdentifier(column, "column name");
|
|
218
|
+
actions.push({
|
|
219
|
+
kind: "add-column",
|
|
220
|
+
target: `${table.qualified}.${column}`,
|
|
221
|
+
sql: `ALTER TABLE "${table.schema}"."${table.table}" ADD COLUMN IF NOT EXISTS "${column}" ${definition};`
|
|
222
|
+
});
|
|
223
|
+
};
|
|
303
224
|
/**
|
|
304
|
-
* Move a
|
|
225
|
+
* Move a column that is only missing because it was renamed.
|
|
305
226
|
*
|
|
306
227
|
* Returns true when it handled the column, so the caller skips the ordinary
|
|
307
|
-
* ADD. Adding
|
|
308
|
-
*
|
|
228
|
+
* ADD. Adding would be the wrong move and a quiet one: the data is in the
|
|
229
|
+
* old column, `ADD COLUMN` creates the new one empty beside it, every
|
|
309
230
|
* statement succeeds, and the relation reads the empty one. A rename is
|
|
310
231
|
* metadata-only in Postgres, keeps the values, and carries the column's
|
|
311
232
|
* indexes and constraints with it.
|
|
312
|
-
*
|
|
313
|
-
* Only ever reached when the new name is absent and the old name is
|
|
314
|
-
* present, so there is nothing to overwrite and nothing to choose between.
|
|
315
233
|
*/
|
|
316
|
-
const renameLegacyColumn = (
|
|
317
|
-
const present = existing.tables.get(
|
|
318
|
-
if (!
|
|
319
|
-
if (present.has(column) || !present.has(
|
|
234
|
+
const renameLegacyColumn = (table, column) => {
|
|
235
|
+
const present = existing.tables.get(table.qualified);
|
|
236
|
+
if (!column.legacyColumn || !present) return false;
|
|
237
|
+
if (present.has(column.column) || !present.has(column.legacyColumn)) return false;
|
|
320
238
|
legacyForeignKeys.push({
|
|
321
|
-
table:
|
|
322
|
-
expected: column,
|
|
323
|
-
legacy:
|
|
239
|
+
table: table.qualified,
|
|
240
|
+
expected: column.column,
|
|
241
|
+
legacy: column.legacyColumn
|
|
324
242
|
});
|
|
325
243
|
actions.push({
|
|
326
244
|
kind: "rename-column",
|
|
327
|
-
target: `${
|
|
328
|
-
sql: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${
|
|
245
|
+
target: `${table.qualified}.${column.column}`,
|
|
246
|
+
sql: `ALTER TABLE "${table.schema}"."${table.table}" RENAME COLUMN "${column.legacyColumn}" TO "${column.column}";`
|
|
329
247
|
});
|
|
330
248
|
return true;
|
|
331
249
|
};
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
250
|
+
/**
|
|
251
|
+
* A `NOT NULL` is safe exactly when it cannot fail against rows that are
|
|
252
|
+
* already there, and there are three ways to know that:
|
|
253
|
+
*
|
|
254
|
+
* - the table is being created by this plan (no rows yet);
|
|
255
|
+
* - the table exists and is empty;
|
|
256
|
+
* - the column arrives with a DEFAULT, which Postgres backfills into every
|
|
257
|
+
* existing row as part of ADD COLUMN.
|
|
258
|
+
*
|
|
259
|
+
* `populatedTables` absent means the caller does not know, and not knowing
|
|
260
|
+
* has to read as "assume rows" — the other direction emits a NOT NULL that
|
|
261
|
+
* is checked against live data and aborts the boot. Written as an explicit
|
|
262
|
+
* `!== undefined` because the optional-chain form quietly says *empty* when
|
|
263
|
+
* the fact is missing, which is the wrong way to be wrong.
|
|
264
|
+
*/
|
|
265
|
+
const tableIsEmpty = (table) => existing.populatedTables !== void 0 && existing.tables.has(table.qualified) && !existing.populatedTables.has(table.qualified);
|
|
266
|
+
/**
|
|
267
|
+
* Plan one column, with the constraints this database can safely take.
|
|
268
|
+
*
|
|
269
|
+
* The definition comes from the same renderer `schema.sql` uses, minus the
|
|
270
|
+
* constraints that are checked against live rows. That subtraction is the
|
|
271
|
+
* whole of what this path is allowed to decide for itself.
|
|
272
|
+
*/
|
|
273
|
+
const planColumn = (table, column) => {
|
|
274
|
+
const key = `${table.qualified}.${column.column}`;
|
|
275
|
+
const columnExists = existing.tables.get(table.qualified)?.has(column.column) === true;
|
|
276
|
+
const fresh = created.has(table.qualified);
|
|
277
|
+
const empty = tableIsEmpty(table);
|
|
278
|
+
const required = !column.nullable;
|
|
279
|
+
const hasDefault = column.default !== void 0;
|
|
280
|
+
const declaredType = renderPgType(column.type);
|
|
281
|
+
const actualType = existing.columnTypes?.get(key);
|
|
282
|
+
if (columnExists && !column.sqlDefinition && actualType && !typesAgree(declaredType, actualType)) columnTypeDrift.push({
|
|
283
|
+
table: table.qualified,
|
|
284
|
+
column: column.column,
|
|
285
|
+
declared: declaredType,
|
|
286
|
+
actual: actualType
|
|
339
287
|
});
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const autoValue = p.autoValue;
|
|
361
|
-
const hasDefault = p.type === "date" && (autoValue === "on_create" || autoValue === "on_update");
|
|
362
|
-
if (hasDefault) definition += " DEFAULT now()";
|
|
363
|
-
const required = p.validation?.required === true;
|
|
364
|
-
const columnKey = `${key}.${column}`;
|
|
365
|
-
const columnExists = existing.tables.get(key)?.has(column) === true;
|
|
366
|
-
const actualType = existing.columnTypes?.get(columnKey);
|
|
367
|
-
if (columnExists && actualType && !typesAgree(declaredType, actualType)) columnTypeDrift.push({
|
|
368
|
-
table: key,
|
|
369
|
-
column,
|
|
370
|
-
declared: declaredType,
|
|
371
|
-
actual: actualType
|
|
372
|
-
});
|
|
373
|
-
const tableIsEmpty = existing.populatedTables !== void 0 && existing.tables.has(key) && !existing.populatedTables.has(key);
|
|
374
|
-
const notNullIsSafe = fresh || tableIsEmpty || hasDefault;
|
|
375
|
-
if (required && !columnExists) if (notNullIsSafe) definition += " NOT NULL";
|
|
376
|
-
else withheldConstraints.push({
|
|
377
|
-
target: columnKey,
|
|
378
|
-
kind: "not-null",
|
|
379
|
-
reason: `"${column}" is required, but "${key}" 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.`,
|
|
380
|
-
remedy: "Backfill the column, then add the constraint — or give the property a default so every existing row gets one."
|
|
288
|
+
const notNullIsSafe = fresh || empty || hasDefault;
|
|
289
|
+
const applicable = {
|
|
290
|
+
...column,
|
|
291
|
+
unique: column.unique && fresh,
|
|
292
|
+
nullable: column.nullable || !(notNullIsSafe || columnExists)
|
|
293
|
+
};
|
|
294
|
+
if (required && !columnExists && !notNullIsSafe) withheldConstraints.push({
|
|
295
|
+
target: key,
|
|
296
|
+
kind: "not-null",
|
|
297
|
+
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.`,
|
|
298
|
+
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."
|
|
299
|
+
});
|
|
300
|
+
addColumn(table, column.column, renderColumnDefinition(applicable));
|
|
301
|
+
if (!columnExists) return;
|
|
302
|
+
if (column.default && column.default.kind !== "identity" && !column.generated) {
|
|
303
|
+
const expression = column.default.kind === "sql" ? column.default.expression : column.default.sql;
|
|
304
|
+
if (!existing.columnDefaults?.has(key)) actions.push({
|
|
305
|
+
kind: "set-default",
|
|
306
|
+
target: key,
|
|
307
|
+
sql: `ALTER TABLE "${table.schema}"."${table.table}" ALTER COLUMN "${column.column}" SET DEFAULT ${expression};`
|
|
381
308
|
});
|
|
382
|
-
addColumn(key, schema, table, column, definition);
|
|
383
|
-
if (columnExists && constraintPolicy === "converge") {
|
|
384
|
-
const isNotNull = existing.notNullColumns?.has(columnKey) === true;
|
|
385
|
-
if (required && !isNotNull) if (tableIsEmpty) actions.push({
|
|
386
|
-
kind: "set-not-null",
|
|
387
|
-
target: columnKey,
|
|
388
|
-
sql: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column}" SET NOT NULL;`
|
|
389
|
-
});
|
|
390
|
-
else withheldConstraints.push({
|
|
391
|
-
target: columnKey,
|
|
392
|
-
kind: "not-null",
|
|
393
|
-
reason: `"${column}" became required, but "${key}" holds rows and any of them with no value would make SET NOT NULL fail.`,
|
|
394
|
-
remedy: "Backfill the column first — `UPDATE … SET \"" + column + "\" = … WHERE \"" + column + "\" IS NULL` — then apply this again."
|
|
395
|
-
});
|
|
396
|
-
if (!required && isNotNull) actions.push({
|
|
397
|
-
kind: "drop-not-null",
|
|
398
|
-
target: columnKey,
|
|
399
|
-
sql: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column}" DROP NOT NULL;`
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
309
|
}
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
310
|
+
if (constraintPolicy !== "converge") return;
|
|
311
|
+
const isNotNull = existing.notNullColumns?.has(key) === true;
|
|
312
|
+
if (required && !isNotNull) if (empty) actions.push({
|
|
313
|
+
kind: "set-not-null",
|
|
314
|
+
target: key,
|
|
315
|
+
sql: `ALTER TABLE "${table.schema}"."${table.table}" ALTER COLUMN "${column.column}" SET NOT NULL;`
|
|
316
|
+
});
|
|
317
|
+
else withheldConstraints.push({
|
|
318
|
+
target: key,
|
|
319
|
+
kind: "not-null",
|
|
320
|
+
reason: `"${column.column}" became required, but "${table.qualified}" holds rows and any of them with no value would make SET NOT NULL fail.`,
|
|
321
|
+
remedy: "Backfill the column first — `UPDATE … SET \"" + column.column + "\" = … WHERE \"" + column.column + "\" IS NULL` — then apply this again."
|
|
322
|
+
});
|
|
323
|
+
if (!required && isNotNull) actions.push({
|
|
324
|
+
kind: "drop-not-null",
|
|
325
|
+
target: key,
|
|
326
|
+
sql: `ALTER TABLE "${table.schema}"."${table.table}" ALTER COLUMN "${column.column}" DROP NOT NULL;`
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
for (const table of collectionTables) {
|
|
330
|
+
assertSafeIdentifier(table.schema, "schema name");
|
|
331
|
+
assertSafeIdentifier(table.table, "table name");
|
|
332
|
+
for (const column of table.columns) {
|
|
333
|
+
if (!isScalarProperty(column)) continue;
|
|
334
|
+
planColumn(table, column);
|
|
335
|
+
}
|
|
336
|
+
for (const column of table.columns) {
|
|
337
|
+
if (column.source.kind !== "auth") continue;
|
|
338
|
+
addColumn(table, column.column, renderColumnDefinition(column));
|
|
409
339
|
}
|
|
410
340
|
}
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const definition = definitions[stamp.column];
|
|
419
|
-
const exists = existing.tables.get(key)?.has(stamp.column) === true;
|
|
420
|
-
const recorded = existing.columnComments?.get(`${key}.${stamp.column}`);
|
|
341
|
+
for (const table of collectionTables) {
|
|
342
|
+
if (!table.search) continue;
|
|
343
|
+
const definitions = new Map(table.columns.filter((c) => c.source.kind === "search").map((c) => [c.column, renderColumnDefinition(c)]));
|
|
344
|
+
for (const stamp of searchColumnStamps(table.search)) {
|
|
345
|
+
const definition = definitions.get(stamp.column);
|
|
346
|
+
const exists = existing.tables.get(table.qualified)?.has(stamp.column) === true;
|
|
347
|
+
const recorded = existing.columnComments?.get(`${table.qualified}.${stamp.column}`);
|
|
421
348
|
if (exists && recorded?.startsWith("rebase:search:v1:") && recorded !== stamp.fingerprint) {
|
|
422
349
|
searchDrift.push({
|
|
423
|
-
table:
|
|
350
|
+
table: table.qualified,
|
|
424
351
|
column: stamp.column,
|
|
425
352
|
found: recorded,
|
|
426
353
|
expected: stamp.fingerprint,
|
|
427
354
|
rebuild: [
|
|
428
|
-
`ALTER TABLE "${
|
|
429
|
-
`ALTER TABLE "${
|
|
355
|
+
`ALTER TABLE "${table.schema}"."${table.table}" DROP COLUMN "${stamp.column}";`,
|
|
356
|
+
`ALTER TABLE "${table.schema}"."${table.table}" ADD COLUMN "${stamp.column}" ${definition};`,
|
|
430
357
|
stamp.sql
|
|
431
358
|
]
|
|
432
359
|
});
|
|
433
360
|
continue;
|
|
434
361
|
}
|
|
435
|
-
addColumn(
|
|
362
|
+
addColumn(table, stamp.column, definition);
|
|
436
363
|
if (exists && recorded === void 0) searchAdopted.push({
|
|
437
|
-
table:
|
|
364
|
+
table: table.qualified,
|
|
438
365
|
column: stamp.column
|
|
439
366
|
});
|
|
440
367
|
if (recorded !== stamp.fingerprint) actions.push({
|
|
441
368
|
kind: "comment-column",
|
|
442
|
-
target: `${
|
|
369
|
+
target: `${table.qualified}.${stamp.column}`,
|
|
443
370
|
sql: stamp.sql
|
|
444
371
|
});
|
|
445
372
|
}
|
|
446
373
|
}
|
|
447
|
-
for (const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
|
|
374
|
+
for (const table of junctionTables) {
|
|
375
|
+
if (created.has(table.qualified)) continue;
|
|
376
|
+
for (const column of table.columns) {
|
|
377
|
+
if (renameLegacyColumn(table, column)) continue;
|
|
378
|
+
if (column.source.kind === "property") {
|
|
379
|
+
planColumn(table, column);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
addColumn(table, column.column, renderPgType(column.type));
|
|
453
383
|
}
|
|
454
384
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
|
|
460
|
-
if (relational.legacyColumn) plannedRelationColumns.get(relKey).add(relational.legacyColumn);
|
|
461
|
-
if (renameLegacyColumn(relKey, relational.schema, relational.table, relational.column, relational.legacyColumn)) continue;
|
|
462
|
-
addColumn(relKey, relational.schema, relational.table, relational.column, relational.type);
|
|
385
|
+
for (const table of collectionTables) for (const column of table.columns) {
|
|
386
|
+
if (column.source.kind !== "relation" && column.source.kind !== "reference") continue;
|
|
387
|
+
if (column.columnOwnedByProperty) continue;
|
|
388
|
+
if (renameLegacyColumn(table, column)) continue;
|
|
389
|
+
planColumn(table, column);
|
|
463
390
|
}
|
|
464
391
|
const knownConstraints = existing.constraints ?? /* @__PURE__ */ new Set();
|
|
465
392
|
const plannedConstraints = /* @__PURE__ */ new Set();
|
|
466
|
-
const
|
|
467
|
-
|
|
393
|
+
for (const table of [...collectionTables, ...junctionTables]) for (const column of table.columns) {
|
|
394
|
+
const fk = column.foreignKey;
|
|
468
395
|
if (!fk) continue;
|
|
469
396
|
const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;
|
|
470
397
|
if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;
|
|
471
398
|
plannedConstraints.add(name);
|
|
472
399
|
actions.push({
|
|
473
400
|
kind: "add-constraint",
|
|
474
|
-
target:
|
|
475
|
-
sql: fk.sql
|
|
401
|
+
target: name,
|
|
402
|
+
sql: `${fk.sql};`
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const concurrent = (schema, table) => !created.has(`${schema}.${table}`);
|
|
406
|
+
for (const table of collectionTables) {
|
|
407
|
+
if (!table.search) continue;
|
|
408
|
+
for (const statement of searchIndexStatements(table.search)) actions.push({
|
|
409
|
+
kind: "create-index",
|
|
410
|
+
target: table.qualified,
|
|
411
|
+
sql: concurrent(table.schema, table.table) ? statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : statement
|
|
476
412
|
});
|
|
477
413
|
}
|
|
478
|
-
const concurrentIndexing = (schema, table) => !created.has(`${schema}.${table}`);
|
|
479
|
-
for (const spec of searchSpecs) for (const statement of searchIndexStatements(spec)) actions.push({
|
|
480
|
-
kind: "create-index",
|
|
481
|
-
target: `${spec.schema}.${spec.table}`,
|
|
482
|
-
sql: concurrentIndexing(spec.schema, spec.table) ? statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : statement
|
|
483
|
-
});
|
|
484
414
|
const vectorIndexSkipped = [];
|
|
485
|
-
for (const
|
|
486
|
-
|
|
487
|
-
for (const spec of
|
|
415
|
+
for (const table of collectionTables) {
|
|
416
|
+
if (!table.vector) continue;
|
|
417
|
+
for (const spec of table.vector.specs) actions.push({
|
|
488
418
|
kind: "create-index",
|
|
489
419
|
target: `${spec.schema}.${spec.table}`,
|
|
490
|
-
sql:
|
|
420
|
+
sql: concurrent(spec.schema, spec.table) ? vectorIndexStatement(spec).replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : vectorIndexStatement(spec)
|
|
491
421
|
});
|
|
492
|
-
vectorIndexSkipped.push(...
|
|
422
|
+
vectorIndexSkipped.push(...table.vector.skipped);
|
|
493
423
|
}
|
|
494
|
-
for (const spec of
|
|
424
|
+
for (const spec of sortIndexSpecs(collectionTables.flatMap((t) => t.indexes))) actions.push({
|
|
495
425
|
kind: "create-index",
|
|
496
426
|
target: `${spec.schema}.${spec.table}`,
|
|
497
427
|
sql: collectionIndexStatement(spec, {
|
|
498
|
-
concurrently:
|
|
428
|
+
concurrently: concurrent(spec.schema, spec.table),
|
|
499
429
|
ifNotExists: true
|
|
500
430
|
})
|
|
501
431
|
});
|
|
432
|
+
for (const table of collectionTables) for (const trigger of table.triggers) {
|
|
433
|
+
if (existing.triggers?.has(`${table.qualified}.${trigger.name}`)) continue;
|
|
434
|
+
for (const statement of triggerStatements(trigger)) actions.push({
|
|
435
|
+
kind: "create-trigger",
|
|
436
|
+
target: `${table.qualified}.${trigger.name}`,
|
|
437
|
+
sql: statement
|
|
438
|
+
});
|
|
439
|
+
}
|
|
502
440
|
const orphanedRequiredColumns = [];
|
|
503
|
-
if (existing.notNullColumns && existing.columnDefaults) for (const
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
if (!live || created.has(key)) continue;
|
|
441
|
+
if (existing.notNullColumns && existing.columnDefaults) for (const table of collectionTables) {
|
|
442
|
+
const live = existing.tables.get(table.qualified);
|
|
443
|
+
if (!live || created.has(table.qualified)) continue;
|
|
507
444
|
const declared = /* @__PURE__ */ new Set();
|
|
508
|
-
for (const
|
|
509
|
-
|
|
445
|
+
for (const column of table.columns) {
|
|
446
|
+
declared.add(column.column);
|
|
447
|
+
if (column.legacyColumn) declared.add(column.legacyColumn);
|
|
448
|
+
}
|
|
510
449
|
for (const column of live) {
|
|
511
|
-
const
|
|
450
|
+
const key = `${table.qualified}.${column}`;
|
|
512
451
|
if (declared.has(column)) continue;
|
|
513
|
-
if (!existing.notNullColumns.has(
|
|
514
|
-
if (existing.columnDefaults.has(
|
|
452
|
+
if (!existing.notNullColumns.has(key)) continue;
|
|
453
|
+
if (existing.columnDefaults.has(key)) continue;
|
|
515
454
|
orphanedRequiredColumns.push({
|
|
516
|
-
table:
|
|
455
|
+
table: table.qualified,
|
|
517
456
|
column,
|
|
518
|
-
slug:
|
|
457
|
+
slug: table.slug
|
|
519
458
|
});
|
|
520
459
|
}
|
|
521
460
|
}
|
|
@@ -531,6 +470,62 @@ function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
|
|
|
531
470
|
orphanedRequiredColumns
|
|
532
471
|
};
|
|
533
472
|
}
|
|
473
|
+
/** What a `CREATE FUNCTION` statement is called, for the action log. */
|
|
474
|
+
var functionTarget = (statement) => {
|
|
475
|
+
if (statement.includes("set_updated_at")) return SET_UPDATED_AT_FN;
|
|
476
|
+
return statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN;
|
|
477
|
+
};
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/schema/ensure-collection-tables.ts
|
|
480
|
+
/**
|
|
481
|
+
* Applying a schema plan to a database that is already running.
|
|
482
|
+
*
|
|
483
|
+
* ## What this module is
|
|
484
|
+
*
|
|
485
|
+
* A managed runtime boots someone else's compiled project against a database it
|
|
486
|
+
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
487
|
+
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
488
|
+
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
489
|
+
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
490
|
+
*
|
|
491
|
+
* Two halves, and only one of them is here. *What the collections ask for* is
|
|
492
|
+
* `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle
|
|
493
|
+
* file; *what the database already has, and which of those statements are safe
|
|
494
|
+
* to run against it* is `schema/plan/diff-plan.ts`. This module reads the
|
|
495
|
+
* catalogue, runs the statements, and reports what happened.
|
|
496
|
+
*
|
|
497
|
+
* ## Why additive-only, forever
|
|
498
|
+
*
|
|
499
|
+
* This runs unattended, against a database with customers' data in it, with no
|
|
500
|
+
* human reading a diff. So it may only ever do things that cannot lose data.
|
|
501
|
+
* See `diff-plan.ts` for the whole of that argument.
|
|
502
|
+
*/
|
|
503
|
+
var ensure_collection_tables_exports = /* @__PURE__ */ __exportAll({
|
|
504
|
+
describeDriverError: () => describeDriverError,
|
|
505
|
+
ensureCollectionTables: () => ensureCollectionTables,
|
|
506
|
+
planCollectionSchemaEnsure: () => planCollectionSchemaEnsure,
|
|
507
|
+
readExistingSchema: () => readExistingSchema,
|
|
508
|
+
readSchemaFactsFor: () => readSchemaFactsFor
|
|
509
|
+
});
|
|
510
|
+
/** The schema a collection lives in, validated before it reaches any DDL. */
|
|
511
|
+
function schemaOf(collection) {
|
|
512
|
+
return assertSafeIdentifier(collection.schema || "public", "schema name");
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Decide what to add. Pure — the caller supplies what exists and runs the
|
|
516
|
+
* result.
|
|
517
|
+
*
|
|
518
|
+
* A plan and a diff: the collections are read once, by `planSchema`, into the
|
|
519
|
+
* same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff
|
|
520
|
+
* subtracts what the database already has. Before that split this function had
|
|
521
|
+
* its own reading of every `Property`, and the audit found twelve places where
|
|
522
|
+
* it disagreed with the two generators — a required `author_id` that was
|
|
523
|
+
* NOT NULL after a push and nullable after a boot among them, on the one path
|
|
524
|
+
* with no developer in the loop.
|
|
525
|
+
*/
|
|
526
|
+
function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
|
|
527
|
+
return diffPlanAgainstCatalogue(planSchema(allCollections, { databaseExtensions: options.databaseExtensions }), existing, { constraints: options.constraints });
|
|
528
|
+
}
|
|
534
529
|
/** Read what the database has, for the schemas the collections live in. */
|
|
535
530
|
async function readExistingSchema(client, schemas) {
|
|
536
531
|
const tables = /* @__PURE__ */ new Map();
|
|
@@ -543,7 +538,8 @@ async function readExistingSchema(client, schemas) {
|
|
|
543
538
|
const notNullColumns = /* @__PURE__ */ new Set();
|
|
544
539
|
const columnTypes = /* @__PURE__ */ new Map();
|
|
545
540
|
const columnDefaults = /* @__PURE__ */ new Set();
|
|
546
|
-
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default
|
|
541
|
+
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,
|
|
542
|
+
numeric_precision, numeric_scale
|
|
547
543
|
FROM information_schema.columns
|
|
548
544
|
WHERE table_schema IN (${inList})`);
|
|
549
545
|
for (const row of columns) {
|
|
@@ -551,9 +547,19 @@ async function readExistingSchema(client, schemas) {
|
|
|
551
547
|
if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
|
|
552
548
|
tables.get(key).add(row.column_name);
|
|
553
549
|
if (row.is_nullable === "NO") notNullColumns.add(`${key}.${row.column_name}`);
|
|
554
|
-
if (row.udt_name)
|
|
550
|
+
if (row.udt_name) {
|
|
551
|
+
const modifier = row.udt_name === "numeric" && row.numeric_precision !== null ? `(${row.numeric_precision},${row.numeric_scale ?? 0})` : "";
|
|
552
|
+
columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);
|
|
553
|
+
}
|
|
555
554
|
if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);
|
|
556
555
|
}
|
|
556
|
+
const triggers = /* @__PURE__ */ new Set();
|
|
557
|
+
const { rows: triggerRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name
|
|
558
|
+
FROM pg_trigger t
|
|
559
|
+
JOIN pg_class c ON t.tgrelid = c.oid
|
|
560
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
561
|
+
WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`);
|
|
562
|
+
for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);
|
|
557
563
|
const populatedTables = /* @__PURE__ */ new Set();
|
|
558
564
|
const { rows: realTables } = await client.query(`SELECT n.nspname AS schema, c.relname AS name
|
|
559
565
|
FROM pg_class c
|
|
@@ -612,7 +618,8 @@ async function readExistingSchema(client, schemas) {
|
|
|
612
618
|
notNullColumns,
|
|
613
619
|
populatedTables,
|
|
614
620
|
columnTypes,
|
|
615
|
-
columnDefaults
|
|
621
|
+
columnDefaults,
|
|
622
|
+
triggers
|
|
616
623
|
};
|
|
617
624
|
}
|
|
618
625
|
/**
|
|
@@ -648,15 +655,14 @@ async function readSchemaFactsFor(client, collections) {
|
|
|
648
655
|
* The error is surfaced with the statement that caused it.
|
|
649
656
|
*/
|
|
650
657
|
async function ensureCollectionTables(client, collections, log, options = {}) {
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
658
|
+
const schema = planSchema(collections, { databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions() });
|
|
659
|
+
const schemas = Array.from(new Set(schema.tables.map((table) => table.schema)));
|
|
660
|
+
for (const name of schemas) {
|
|
661
|
+
assertSafeIdentifier(name, "schema name");
|
|
662
|
+
if (name !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${name}";`);
|
|
655
663
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()
|
|
659
|
-
});
|
|
664
|
+
if (schema.tables.some((table) => table.triggers.length > 0)) await client.query(`CREATE SCHEMA IF NOT EXISTS "${REBASE_SCHEMA}";`);
|
|
665
|
+
const plan = diffPlanAgainstCatalogue(schema, await readExistingSchema(client, schemas), { constraints: options.constraints });
|
|
660
666
|
const failures = [];
|
|
661
667
|
for (const legacy of plan.legacyForeignKeys) {
|
|
662
668
|
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.`;
|
|
@@ -817,4 +823,4 @@ async function applyAction(client, action) {
|
|
|
817
823
|
//#endregion
|
|
818
824
|
export { readSchemaFactsFor as a, readExistingSchema as i, ensure_collection_tables_exports as n, planCollectionSchemaEnsure as r, describeDriverError as t };
|
|
819
825
|
|
|
820
|
-
//# sourceMappingURL=ensure-collection-tables-
|
|
826
|
+
//# sourceMappingURL=ensure-collection-tables-Bkvehxdm.js.map
|