@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.
Files changed (96) hide show
  1. package/dist/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
  2. package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +81 -7
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
  6. package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
  7. package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
  8. package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
  9. package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
  10. package/dist/cli-errors-C3g_kHBw.js.map +1 -0
  11. package/dist/cli-errors.d.ts +19 -0
  12. package/dist/cli-helpers.d.ts +47 -0
  13. package/dist/cli.js +233 -19
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
  16. package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
  17. package/dist/collections/validate-relations.d.ts +7 -0
  18. package/dist/column-plan-helpers-CpILzHJS.js +145 -0
  19. package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
  20. package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
  21. package/dist/doctor-D5SrGJ5P.js.map +1 -0
  22. package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
  23. package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
  24. package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
  25. package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
  26. package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
  27. package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
  28. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
  29. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
  30. package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
  31. package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
  33. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
  34. package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
  35. package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
  36. package/dist/history/HistoryService.d.ts +23 -4
  37. package/dist/index.es.js +1991 -580
  38. package/dist/index.es.js.map +1 -1
  39. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
  40. package/dist/plan-schema-Hgl62S-w.js +1531 -0
  41. package/dist/plan-schema-Hgl62S-w.js.map +1 -0
  42. package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
  43. package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
  44. package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
  45. package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
  46. package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
  47. package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
  48. package/dist/schema/catalogue-schema.d.ts +97 -0
  49. package/dist/schema/collection-index.d.ts +9 -0
  50. package/dist/schema/column-plan-helpers.d.ts +125 -0
  51. package/dist/schema/config-relations.d.ts +32 -0
  52. package/dist/schema/doctor-cli.js +4 -4
  53. package/dist/schema/doctor.d.ts +9 -0
  54. package/dist/schema/dynamic-tables.d.ts +27 -6
  55. package/dist/schema/ensure-collection-tables.d.ts +31 -268
  56. package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
  57. package/dist/schema/generate-drizzle-schema.js +1 -1
  58. package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
  59. package/dist/schema/generate-postgres-ddl.js +8 -2
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  61. package/dist/schema/generated-column-conflicts.d.ts +84 -0
  62. package/dist/schema/generated-schema-diff.d.ts +56 -0
  63. package/dist/schema/generated-schema-staleness.d.ts +33 -0
  64. package/dist/schema/introspect-db-logic.d.ts +2 -0
  65. package/dist/schema/plan/diff-plan.d.ts +220 -0
  66. package/dist/schema/plan/plan-schema.d.ts +84 -0
  67. package/dist/schema/plan/render-ddl.d.ts +62 -0
  68. package/dist/schema/plan/render-drizzle.d.ts +31 -0
  69. package/dist/schema/plan/types.d.ts +410 -0
  70. package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
  71. package/dist/schema/relation-names.d.ts +28 -0
  72. package/dist/schema/search-column.d.ts +9 -0
  73. package/dist/security/rls-enforcement.d.ts +17 -0
  74. package/dist/services/FetchService.d.ts +176 -36
  75. package/dist/services/PersistService.d.ts +17 -4
  76. package/dist/services/RelationService.d.ts +11 -3
  77. package/dist/services/collection-helpers.d.ts +16 -2
  78. package/dist/services/dataService.d.ts +29 -2
  79. package/dist/services/field-op-sql.d.ts +16 -0
  80. package/dist/services/field-viewer.d.ts +19 -0
  81. package/dist/services/realtimeService.d.ts +35 -1
  82. package/dist/services/row-pipeline.d.ts +29 -5
  83. package/dist/services/soft-delete.d.ts +73 -0
  84. package/dist/utils/pg-error-utils.d.ts +41 -1
  85. package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
  86. package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
  87. package/package.json +18 -10
  88. package/dist/cli-errors-DxN4yzdH.js.map +0 -1
  89. package/dist/doctor-D7HWmGzs.js.map +0 -1
  90. package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
  91. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
  92. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
  93. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
  94. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
  95. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
  96. 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 { C as searchIndexStatements, S as searchHelperFunctions, _ as assertSearchIsPostgresOnly, b as searchColumnStamps, c as isIdProperty, d as planRelationalColumns, f as quoteSqlLiteral, g as SEARCH_UNACCENT_FN, h as SEARCH_TEXT_FN, p as resolveColumnName, s as getSqlColumnType, u as planJunctionTables, v as buildSearchColumnSpec, x as searchExtensionStatements } from "./generate-postgres-ddl-logic-CtnpIBVv.js";
5
- import { l as extractCauseMessage, u as extractPgError } from "./cli-errors-DxN4yzdH.js";
6
- import { i as isAuthCollection, n as authUsersColumnDefinition, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
7
- import { c as vectorExtensionStatement, l as vectorIndexStatement, n as buildVectorColumnSpecs, o as vectorExtensionDeclared, r as buildVectorIndexPlan, s as vectorExtensionHint } from "./vector-index-DkWxm2de.js";
8
- import { r as collectionIndexStatement, t as buildCollectionIndexPlan } from "./collection-index-BHgT_TKd.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-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 { declaredDatabaseExtensions, isPostgresCollectionConfig } from "@rebasepro/types";
11
- import { getTableName, relationalCollections } from "@rebasepro/common";
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
- return a === b;
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/ensure-collection-tables.ts
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 schemaOf(collection) {
177
- return assertSafeIdentifier(isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public", "schema name");
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
- * Decide what to add. Pure the caller supplies what exists and runs the result.
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
- assertSearchIsPostgresOnly(allCollections);
223
- const collections = relationalCollections(allCollections);
224
- const actions = [];
225
- const plannedEnums = /* @__PURE__ */ new Set();
226
- for (const collection of collections) for (const { name, values } of requiredEnums(collection)) {
227
- if (existing.enums.has(name) || plannedEnums.has(name)) {
228
- const current = existing.enumValues?.get(name);
229
- if (!current || plannedEnums.has(name)) continue;
230
- const [schema, typeName] = name.split(".");
231
- for (const value of values) {
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: `${name}.${value}`,
236
- sql: `ALTER TYPE "${schema}"."${typeName}" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`
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: name,
246
- sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});`
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 collection of collections) {
274
- const key = qualified(collection);
275
- if (existing.tables.has(key) || created.has(key)) continue;
276
- created.add(key);
277
- const schema = schemaOf(collection);
278
- const table = tableOf(collection);
279
- const idEntry = Object.entries(collection.properties ?? {}).find(([n, p]) => isIdProperty(n, p, collection));
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: key,
288
- sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${table}" (${idDef});`
201
+ target: table.qualified,
202
+ sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${name}" (${definition});`
289
203
  });
290
204
  }
291
- const junctions = planJunctionTables(collections);
292
- for (const junction of junctions) {
293
- const key = `${junction.schema}.${junction.table}`;
294
- if (existing.tables.has(key) || created.has(key)) continue;
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: key,
299
- sql: junction.createTable
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 legacyForeignKeys = [];
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 relation column that is only missing because it was renamed.
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 here would be the wrong move and a quiet one: the data is in
308
- * the old column, `ADD COLUMN` creates the new one empty beside it, every
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 = (key, schema, table, column, legacyName) => {
317
- const present = existing.tables.get(key);
318
- if (!legacyName || !present) return false;
319
- if (present.has(column) || !present.has(legacyName)) return false;
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: key,
322
- expected: column,
323
- legacy: legacyName
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: `${key}.${column}`,
328
- sql: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${legacyName}" TO "${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
- const addColumn = (key, schema, table, column, definition) => {
333
- if (existing.tables.get(key)?.has(column)) return;
334
- assertSafeIdentifier(column, "column name");
335
- actions.push({
336
- kind: "add-column",
337
- target: `${key}.${column}`,
338
- sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${definition};`
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
- for (const collection of collections) {
342
- const key = qualified(collection);
343
- const schema = schemaOf(collection);
344
- const table = tableOf(collection);
345
- const fresh = created.has(key);
346
- const auth = isAuthCollection(collection);
347
- for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
348
- const p = prop;
349
- if (isIdProperty(propName, p, collection)) continue;
350
- if (p.type === "reference" || p.type === "relation") continue;
351
- const column = resolveColumnName(propName, p);
352
- const authDefinition = auth ? authUsersColumnDefinition(column) : void 0;
353
- if (authDefinition) {
354
- addColumn(key, schema, table, column, authDefinition);
355
- continue;
356
- }
357
- const declaredType = getSqlColumnType(propName, p, collection, collections);
358
- let definition = declaredType;
359
- if (fresh && p.validation?.unique) definition += " UNIQUE";
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 (auth) {
404
- const declared = new Set(Object.entries(collection.properties ?? {}).map(([name, prop]) => resolveColumnName(name, prop)));
405
- for (const spec of AUTH_USERS_COLUMNS) {
406
- if (declared.has(spec.column)) continue;
407
- addColumn(key, schema, table, spec.column, authUsersColumnSql(spec));
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 searchDrift = [];
412
- const searchAdopted = [];
413
- for (const spec of searchSpecs) {
414
- const key = `${spec.schema}.${spec.table}`;
415
- const definitions = { [spec.column]: `tsvector GENERATED ALWAYS AS (${spec.expression}) STORED` };
416
- if (spec.fuzzy) definitions[spec.fuzzy.column] = `text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED`;
417
- for (const stamp of searchColumnStamps(spec)) {
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: key,
350
+ table: table.qualified,
424
351
  column: stamp.column,
425
352
  found: recorded,
426
353
  expected: stamp.fingerprint,
427
354
  rebuild: [
428
- `ALTER TABLE "${spec.schema}"."${spec.table}" DROP COLUMN "${stamp.column}";`,
429
- `ALTER TABLE "${spec.schema}"."${spec.table}" ADD COLUMN "${stamp.column}" ${definition};`,
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(key, spec.schema, spec.table, stamp.column, definition);
362
+ addColumn(table, stamp.column, definition);
436
363
  if (exists && recorded === void 0) searchAdopted.push({
437
- table: key,
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: `${key}.${stamp.column}`,
369
+ target: `${table.qualified}.${stamp.column}`,
443
370
  sql: stamp.sql
444
371
  });
445
372
  }
446
373
  }
447
- for (const junction of junctions) {
448
- const key = `${junction.schema}.${junction.table}`;
449
- if (created.has(key)) continue;
450
- for (const column of junction.columns) {
451
- if (renameLegacyColumn(key, junction.schema, junction.table, column.name, column.legacyName)) continue;
452
- addColumn(key, junction.schema, junction.table, column.name, column.type);
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 plannedRelationColumns = /* @__PURE__ */ new Map();
456
- for (const relational of planRelationalColumns(collections)) {
457
- const relKey = `${relational.schema}.${relational.table}`;
458
- if (!plannedRelationColumns.has(relKey)) plannedRelationColumns.set(relKey, /* @__PURE__ */ new Set());
459
- plannedRelationColumns.get(relKey).add(relational.column);
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 foreignKeys = [...planRelationalColumns(collections).map((r) => r.foreignKey), ...junctions.flatMap((j) => j.foreignKeys)];
467
- for (const fk of foreignKeys) {
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: `${fk.schema}.${fk.table}.${fk.constraintName}`,
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 collection of collections) {
486
- const plan = buildVectorIndexPlan(collection, resolveColumnName);
487
- for (const spec of plan.specs) actions.push({
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: concurrentIndexing(spec.schema, spec.table) ? vectorIndexStatement(spec).replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : vectorIndexStatement(spec)
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(...plan.skipped);
422
+ vectorIndexSkipped.push(...table.vector.skipped);
493
423
  }
494
- for (const spec of buildCollectionIndexPlan(collections, resolveColumnName)) actions.push({
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: concurrentIndexing(spec.schema, spec.table),
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 collection of collections) {
504
- const key = qualified(collection);
505
- const live = existing.tables.get(key);
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 [propName, prop] of Object.entries(collection.properties ?? {})) declared.add(resolveColumnName(propName, prop));
509
- for (const column of plannedRelationColumns.get(key) ?? []) declared.add(column);
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 columnKey = `${key}.${column}`;
450
+ const key = `${table.qualified}.${column}`;
512
451
  if (declared.has(column)) continue;
513
- if (!existing.notNullColumns.has(columnKey)) continue;
514
- if (existing.columnDefaults.has(columnKey)) continue;
452
+ if (!existing.notNullColumns.has(key)) continue;
453
+ if (existing.columnDefaults.has(key)) continue;
515
454
  orphanedRequiredColumns.push({
516
- table: key,
455
+ table: table.qualified,
517
456
  column,
518
- slug: collection.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) columnTypes.set(`${key}.${row.column_name}`, 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 schemas = Array.from(/* @__PURE__ */ new Set([...collections.map(schemaOf), ...planJunctionTables(collections).map((j) => j.schema)]));
652
- for (const schema of schemas) {
653
- assertSafeIdentifier(schema, "schema name");
654
- if (schema !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}";`);
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
- const plan = planCollectionSchemaEnsure(collections, await readExistingSchema(client, schemas), {
657
- ...options,
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-D4wJzM4y.js.map
826
+ //# sourceMappingURL=ensure-collection-tables-Bkvehxdm.js.map