@rebasepro/server-postgres 0.17.3 → 0.18.1

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 (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
@@ -1,32 +1,13 @@
1
1
  import { createRequire as __createRequire } from "module";
2
- import "process";
3
2
  __createRequire(import.meta.url);
4
- import { u as __exportAll } from "./connection-GOKU3Hu5.js";
5
- import { B as getPolicyNamesForRule, C as getTableName, I as generateForeignKeyName, L as legacyForeignKeyName, R as toPostgresIdentifier, T as resolveCollectionRelations, W as toSnakeCase, Z as isManyToMany, _ as securityRuleToConditions, b as findRelation, d as getJunctionCollectionConfig, f as getJunctionSecurityRules, g as policyToPostgres, h as getInjectedSecurityRules, i as collectionIndexStatements, m as getEffectiveSecurityRules, n as buildCollectionIndexSpecs, p as resolveJunctionSpecs, r as collectionIndexStatement, t as buildCollectionIndexPlan, u as relationalCollections } from "./collection-index-BRUg10H5.js";
6
- import { l as isPostgresCollectionConfig, r as REBASE_SCHEMA, t as declaredDatabaseExtensions } from "./src-DiB5RP2Z.js";
7
- import { isConcurrentDdlRace, isDuplicateObjectRace, logger } from "@rebasepro/server";
3
+ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
+ import { i as isAuthCollection, n as authUsersColumnDefinition, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
5
+ import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorObjectNames, i as vectorColumnDefinition, n as buildVectorColumnSpecs, o as vectorExtensionDeclared, r as buildVectorIndexPlan, t as VECTOR_EXTENSION_OPT_IN, u as vectorIndexStatements } from "./vector-index-DkWxm2de.js";
6
+ import { i as collectionIndexStatements, n as buildCollectionIndexSpecs } from "./collection-index-BHgT_TKd.js";
7
+ import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, REBASE_SCHEMA, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
8
+ import { findRelation, getEffectiveSecurityRules, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getTableName, policyToPostgres, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, resolveStringColumnLength, securityRuleToConditions } from "@rebasepro/common";
9
+ import { generateForeignKeyName, getPolicyNamesForRule, legacyForeignKeyName, toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
8
10
  import { createHash } from "node:crypto";
9
- /**
10
- * How wide a `varchar`/`char` column should be for a given property.
11
- *
12
- * One definition, three call sites, because they used to disagree. For the same
13
- * `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
14
- * while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
15
- * reads as *unbounded* — so which of the two you ran decided whether the column
16
- * had a limit at all. Introspection then dropped the length entirely, so reading
17
- * an existing `character varying(500)` column back and regenerating it produced
18
- * a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
19
- *
20
- * `validation.max` is the property's own statement about how long the value may
21
- * be, so it is the only sensible source for the column's width — and it keeps
22
- * the constraint the database enforces in step with the one the app enforces,
23
- * rather than inventing a second, different limit underneath it.
24
- */
25
- function resolveStringColumnLength(prop) {
26
- const max = prop.validation?.max;
27
- return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
28
- }
29
- //#endregion
30
11
  //#region src/schema/search-column.ts
31
12
  /**
32
13
  * The one place a collection's `search` block becomes SQL.
@@ -171,9 +152,9 @@ var rawTextSql = (field) => {
171
152
  const col = `"${field.column}"`;
172
153
  if (field.kind === "text") return `coalesce(${col}, '')`;
173
154
  if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
174
- return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote$1(field.jsonPath[0])}` : `${col} #> ${quote$1(`{${field.jsonPath.join(",")}}`)}`}, '{}'::jsonb))`;
155
+ return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote(field.jsonPath[0])}` : `${col} #> ${quote(`{${field.jsonPath.join(",")}}`)}`}, '{}'::jsonb))`;
175
156
  };
176
- var quote$1 = (v) => `'${v.replace(/'/g, "''")}'`;
157
+ var quote = (v) => `'${v.replace(/'/g, "''")}'`;
177
158
  /**
178
159
  * Resolve and validate one declared field path.
179
160
  *
@@ -184,7 +165,7 @@ var quote$1 = (v) => `'${v.replace(/'/g, "''")}'`;
184
165
  */
185
166
  var resolveField = (entry, collection, cfg) => {
186
167
  const path = typeof entry === "string" ? entry : entry.path;
187
- const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? "B";
168
+ const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? DEFAULT_SEARCH_WEIGHT;
188
169
  const where = `${collection.slug}.search`;
189
170
  if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
190
171
  const [head, ...rest] = path.split(".");
@@ -203,14 +184,14 @@ var resolveField = (entry, collection, cfg) => {
203
184
  jsonPath: rest,
204
185
  kind: classified.kind
205
186
  }), cfg.unaccent === true);
206
- const language = cfg.language ?? "simple";
187
+ const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;
207
188
  return {
208
189
  path,
209
190
  column,
210
191
  jsonPath: rest,
211
192
  kind: classified.kind,
212
193
  weight,
213
- sql: `setweight(to_tsvector(${quote$1(language)}, ${textSql}), ${quote$1(weight)})`,
194
+ sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
214
195
  textSql
215
196
  };
216
197
  };
@@ -227,7 +208,7 @@ var buildSearchColumnSpec = (collection) => {
227
208
  if (!Array.isArray(cfg.fields) || cfg.fields.length === 0) throw new SearchConfigError(`${collection.slug}.search: \`fields\` is empty. Name the properties to index, or remove the \`search\` block to keep the default ILIKE behaviour.`);
228
209
  const table = getTableName(collection);
229
210
  const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
230
- const column = cfg.column ?? "search_vector";
211
+ const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;
231
212
  if (collection.properties?.[column]) throw new SearchConfigError(`${collection.slug}.search: the generated column "${column}" collides with a declared property of the same name. Set \`search.column\` to something else.`);
232
213
  const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
233
214
  const seen = /* @__PURE__ */ new Set();
@@ -242,7 +223,7 @@ var buildSearchColumnSpec = (collection) => {
242
223
  schema,
243
224
  table,
244
225
  column,
245
- language: cfg.language ?? "simple",
226
+ language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,
246
227
  unaccent: cfg.unaccent === true,
247
228
  fields,
248
229
  expression: fields.map((f) => f.sql).join(" || "),
@@ -256,7 +237,7 @@ var buildSearchColumnSpec = (collection) => {
256
237
  column: fuzzyColumn,
257
238
  expression: fields.map((f) => f.textSql).join(" || ' ' || "),
258
239
  indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
259
- threshold: cfg.fuzzyThreshold ?? .3
240
+ threshold: cfg.fuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD
260
241
  };
261
242
  }
262
243
  return spec;
@@ -343,7 +324,7 @@ var searchColumnStamps = (spec) => {
343
324
  column,
344
325
  expression,
345
326
  fingerprint,
346
- sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote$1(fingerprint)};`
327
+ sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
347
328
  };
348
329
  };
349
330
  const stamps = [stamp(spec.column, spec.expression)];
@@ -361,20 +342,27 @@ var searchColumnStamps = (spec) => {
361
342
  * and the operator reading the failure is the person who changed the block.
362
343
  */
363
344
  var searchStampGuards = (spec) => searchColumnStamps(spec).map((stamp) => {
364
- const relation = quote$1(`"${spec.schema}"."${spec.table}"`);
345
+ const relation = quote(`"${spec.schema}"."${spec.table}"`);
365
346
  return `DO $rebase_search$
366
347
  DECLARE recorded text;
367
348
  BEGIN
368
349
  SELECT col_description(a.attrelid, a.attnum) INTO recorded
369
350
  FROM pg_attribute a
370
- WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote$1(stamp.column)} AND NOT a.attisdropped;
371
- IF recorded LIKE ${quote$1(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote$1(stamp.fingerprint)} THEN
351
+ WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote(stamp.column)} AND NOT a.attisdropped;
352
+ IF recorded LIKE ${quote(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote(stamp.fingerprint)} THEN
372
353
  RAISE EXCEPTION 'Rebase: the search block for ${spec.schema}.${spec.table} changed after the generated column "${stamp.column}" was built (recorded %, expected ${stamp.fingerprint}). Postgres cannot alter a generated expression in place. Drop the column and re-apply this file — it rewrites the table and rebuilds the index: ALTER TABLE ${relation.slice(1, -1)} DROP COLUMN "${stamp.column}";', recorded;
373
354
  END IF;
374
355
  END
375
356
  $rebase_search$;`;
376
357
  });
377
358
  /**
359
+ * The index names the spec creates.
360
+ *
361
+ * Needed by name, not just by statement, so Atlas can be told to exclude them
362
+ * from its diff — see `searchExcludePatterns`.
363
+ */
364
+ var searchIndexNames = (spec) => spec.fuzzy ? [spec.indexName, spec.fuzzy.indexName] : [spec.indexName];
365
+ /**
378
366
  * The generated column names a collection's search block adds, if any.
379
367
  *
380
368
  * These are physical columns on the table, so `SELECT *` returns them. They are
@@ -440,381 +428,27 @@ var excludedColumnNames = (tableColumns, collection) => {
440
428
  return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
441
429
  };
442
430
  //#endregion
443
- //#region src/schema/auth-users-columns.ts
444
- /**
445
- * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
446
- * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
447
- * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
448
- * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
449
- * table that already holds a longer row.
450
- */
451
- var AUTH_USERS_COLUMNS = [
452
- {
453
- column: "email",
454
- type: "TEXT",
455
- notNull: true
456
- },
457
- {
458
- column: "display_name",
459
- type: "TEXT"
460
- },
461
- {
462
- column: "photo_url",
463
- type: "TEXT"
464
- },
465
- {
466
- column: "roles",
467
- type: "TEXT[]",
468
- default: "'{}'",
469
- notNull: true
470
- },
471
- {
472
- column: "password_hash",
473
- type: "TEXT"
474
- },
475
- {
476
- column: "email_verified",
477
- type: "BOOLEAN",
478
- default: "FALSE",
479
- notNull: true
480
- },
481
- {
482
- column: "email_verification_token",
483
- type: "TEXT"
484
- },
485
- {
486
- column: "email_verification_sent_at",
487
- type: "TIMESTAMP WITH TIME ZONE"
488
- },
489
- {
490
- column: "is_anonymous",
491
- type: "BOOLEAN",
492
- default: "FALSE",
493
- notNull: true
494
- },
495
- {
496
- column: "metadata",
497
- type: "JSONB",
498
- default: "'{}'",
499
- notNull: true
500
- },
501
- {
502
- column: "tokens_valid_after",
503
- type: "TIMESTAMP WITH TIME ZONE"
504
- },
505
- {
506
- column: "created_at",
507
- type: "TIMESTAMP WITH TIME ZONE",
508
- default: "NOW()",
509
- notNull: true
510
- },
511
- {
512
- column: "updated_at",
513
- type: "TIMESTAMP WITH TIME ZONE",
514
- default: "NOW()",
515
- notNull: true
516
- }
517
- ];
518
- var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
519
- /** Type + inline constraints, as they appear after the column name. */
520
- function authUsersColumnSql(spec) {
521
- return [
522
- spec.type,
523
- spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
524
- spec.notNull ? "NOT NULL" : ""
525
- ].filter(Boolean).join(" ");
526
- }
527
- /**
528
- * The auth-owned definition for a physical column name, or `undefined` when
529
- * auth does not own it.
530
- *
531
- * Callers pass the RESOLVED column name (after `columnName` mapping), because
532
- * that is the only name the three creators agree on: the scaffold's users
533
- * collection spells the property `displayName` and the column `display_name`.
534
- */
535
- function authUsersColumnDefinition(column) {
536
- const spec = BY_COLUMN.get(column);
537
- return spec ? authUsersColumnSql(spec) : void 0;
538
- }
539
- /**
540
- * Whether a collection is an auth collection, i.e. whether the definitions in
541
- * this module apply to its table at all.
542
- *
543
- * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
544
- * that one takes a `CollectionConfig`, this one is called from DDL code paths
545
- * that hold looser objects, and both spellings must accept `auth: true` as well
546
- * as `auth: { enabled: true }`.
547
- */
548
- function isAuthCollection(collection) {
549
- const auth = collection?.auth;
550
- if (auth === true) return true;
551
- return typeof auth === "object" && auth !== null && auth.enabled === true;
552
- }
553
- //#endregion
554
- //#region src/schema/vector-index.ts
555
- /**
556
- * The widest `vector` pgvector will build an HNSW or IVFFlat index over.
557
- * Storage and exact search are unaffected by this limit.
558
- */
559
- var MAX_INDEXABLE_VECTOR_DIMENSIONS = 2e3;
560
- /**
561
- * Operator class per distance. These strings are part of the database contract:
562
- * they appear in `CREATE INDEX`, so renaming one renames an index.
563
- */
564
- var OPERATOR_CLASS = {
565
- cosine: "vector_cosine_ops",
566
- l2: "vector_l2_ops",
567
- inner_product: "vector_ip_ops"
568
- };
569
- /** Short, stable tag per distance, used to name the index. */
570
- var DISTANCE_TAG = {
571
- cosine: "cosine",
572
- l2: "l2",
573
- inner_product: "ip"
574
- };
575
- var VectorIndexConfigError = class extends Error {
576
- constructor(message) {
577
- super(message);
578
- this.name = "VectorIndexConfigError";
579
- }
580
- };
581
- var isVectorProperty = (prop) => !!prop && typeof prop === "object" && prop.type === "vector";
582
- var asDistances = (config, label) => {
583
- const raw = config.distance ?? "cosine";
584
- const list = Array.isArray(raw) ? raw : [raw];
585
- if (list.length === 0) throw new VectorIndexConfigError(`${label}: \`index.distance\` is an empty array. Name at least one distance, or set \`index: false\` to create no index.`);
586
- const seen = /* @__PURE__ */ new Set();
587
- for (const distance of list) {
588
- if (!(distance in OPERATOR_CLASS)) throw new VectorIndexConfigError(`${label}: \`index.distance\` is "${distance}", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map((d) => `"${d}"`).join(", ")}.`);
589
- if (seen.has(distance)) throw new VectorIndexConfigError(`${label}: \`index.distance\` lists "${distance}" twice.`);
590
- seen.add(distance);
591
- }
592
- return list;
593
- };
594
- var assertPositiveInteger = (value, key, label) => {
595
- if (value === void 0) return;
596
- if (!Number.isInteger(value) || value <= 0) throw new VectorIndexConfigError(`${label}: \`index.${key}\` is ${JSON.stringify(value)}. It must be a positive integer.`);
597
- };
598
- /**
599
- * Index parameters for one method. Parameters belonging to the *other* method
600
- * are rejected rather than ignored, because a silently dropped `lists` on an
601
- * HNSW index reads, from the config, exactly like a tuned index.
602
- */
603
- var parametersFor = (method, config, label) => {
604
- assertPositiveInteger(config.m, "m", label);
605
- assertPositiveInteger(config.efConstruction, "efConstruction", label);
606
- assertPositiveInteger(config.lists, "lists", label);
607
- if (method === "hnsw") {
608
- if (config.lists !== void 0) throw new VectorIndexConfigError(`${label}: \`index.lists\` only applies to \`method: "ivfflat"\`. Remove it, or switch the method.`);
609
- const params = [];
610
- if (config.m !== void 0) params.push(["m", config.m]);
611
- if (config.efConstruction !== void 0) params.push(["ef_construction", config.efConstruction]);
612
- return params;
613
- }
614
- for (const key of ["m", "efConstruction"]) if (config[key] !== void 0) throw new VectorIndexConfigError(`${label}: \`index.${key}\` only applies to \`method: "hnsw"\`. Remove it, or switch the method.`);
615
- return config.lists !== void 0 ? [["lists", config.lists]] : [];
616
- };
617
- /**
618
- * Every ANN index a collection's vector properties call for.
619
- *
620
- * `resolveColumn` is passed in rather than imported so that this module stays
621
- * free of the DDL generator, which imports *it*. Both callers hand it the same
622
- * `resolveColumnName`, and a contract test asserts the names agree.
623
- */
624
- var buildVectorIndexPlan = (collection, resolveColumn) => {
625
- const specs = [];
626
- const skipped = [];
627
- const properties = collection.properties ?? {};
628
- const table = getTableName(collection);
629
- const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
630
- for (const [propName, prop] of Object.entries(properties)) {
631
- if (!isVectorProperty(prop)) continue;
632
- if (prop.index === false) continue;
633
- const label = `${collection.slug}.${propName}`;
634
- const column = resolveColumn(propName, prop);
635
- const config = prop.index ?? {};
636
- const method = config.method ?? "hnsw";
637
- if (method !== "hnsw" && method !== "ivfflat") throw new VectorIndexConfigError(`${label}: \`index.method\` is "${method}". Use "hnsw" or "ivfflat".`);
638
- const distances = asDistances(config, label);
639
- const parameters = parametersFor(method, config, label);
640
- if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) throw new VectorIndexConfigError(`${label}: \`dimensions\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`);
641
- if (prop.dimensions > 2e3) {
642
- skipped.push({
643
- schema,
644
- table,
645
- column,
646
- dimensions: prop.dimensions,
647
- reason: `pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ${label} declares ${prop.dimensions}. The column works and \`vectorSearch\` still answers, as an exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) or set \`index: false\` to state that the scan is intended.`
648
- });
649
- continue;
650
- }
651
- for (const distance of distances) specs.push({
652
- schema,
653
- table,
654
- column,
655
- indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),
656
- method,
657
- distance,
658
- operatorClass: OPERATOR_CLASS[distance],
659
- parameters
660
- });
661
- }
662
- return {
663
- specs,
664
- skipped
665
- };
666
- };
667
- /**
668
- * The `CREATE INDEX` for one spec.
669
- *
670
- * `CONCURRENTLY` is deliberately absent, for the same reason it is absent from
671
- * `searchIndexStatements`: this form is replayed as part of a migration, where
672
- * a concurrent build is not allowed. The boot-time ensure rewrites it — see
673
- * `ensureCollectionTables`.
674
- */
675
- var vectorIndexStatement = (spec) => {
676
- const params = spec.parameters.length ? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(", ")})` : "";
677
- return `CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING ${spec.method} ("${spec.column}" ${spec.operatorClass})${params};`;
678
- };
679
- /** Every statement for a plan, in a stable order. */
680
- var vectorIndexStatements = (plan) => plan.specs.map(vectorIndexStatement);
681
- var quote = (v) => `'${v.replace(/'/g, "''")}'`;
682
- /**
683
- * The schema pgvector's types are installed into.
684
- *
685
- * `WITH SCHEMA public` for the same load-bearing reason `searchExtensionStatements`
686
- * gives: an unqualified `CREATE EXTENSION` lands in the first schema on
687
- * `search_path`, which is `"$user", public` — and the scaffold's role is named
688
- * `rebase`, the same as a schema the generator creates. Left to itself the
689
- * extension would install into `rebase`, and every unqualified `VECTOR(n)`
690
- * below would fail to resolve.
691
- */
692
- var VECTOR_EXTENSION_SCHEMA = "public";
693
- /** The extension name a database has to name to let Rebase install pgvector. */
694
- var VECTOR_EXTENSION = "vector";
695
- /**
696
- * How a project says Rebase may install pgvector, quoted into the messages that
697
- * have to name it. Spelled once so the option and the advice cannot drift.
698
- */
699
- var VECTOR_EXTENSION_OPT_IN = `database({ extensions: ["${VECTOR_EXTENSION}"] })`;
700
- /** Did the project give Rebase leave to install pgvector? */
701
- var vectorExtensionDeclared = (extensions) => (extensions ?? []).includes(VECTOR_EXTENSION);
702
- /**
703
- * Install pgvector — emitted only when the database asked for it.
704
- *
705
- * Opt-in because installing an extension is a decision with a deployment behind
706
- * it: the image has to ship the library, the role has to be allowed to install
707
- * it, and a managed provider has to have it on an allow-list. None of that is
708
- * visible from inside the connection, so Rebase does not decide it. See
709
- * `DatabaseOptions.extensions`.
710
- *
711
- * Withholding the statement is not withholding the *column*: the column is
712
- * still created, and Postgres refuses it with `type "vector" does not exist`
713
- * on a database where pgvector was never installed by hand. That error is the
714
- * one this design accepts, and `vectorExtensionHint` is what makes it name
715
- * {@link VECTOR_EXTENSION_OPT_IN} rather than nothing.
716
- */
717
- var vectorExtensionStatement = () => `CREATE EXTENSION IF NOT EXISTS ${VECTOR_EXTENSION} WITH SCHEMA ${VECTOR_EXTENSION_SCHEMA};`;
718
- /**
719
- * The missing-pgvector explanation, appended to whichever error revealed it.
720
- *
721
- * Two readers, needing opposite things, which is why the branch is on the error
722
- * text rather than on the configuration:
723
- *
724
- * - **`type "vector" does not exist`** — nobody opted in and the database has
725
- * no pgvector. The fix is one line of config they cannot guess, so naming
726
- * the option *is* the hint.
727
- * - **the install itself failed** — the config is already right, and repeating
728
- * the option would send them to edit a correct line. What is missing is the
729
- * library on the server, or the grant.
730
- *
731
- * Lives here rather than beside either caller because both need it: the boot
732
- * ensure raises the first through its action applier, and `rebase db push`
733
- * raises it out of `applyVectorDdl`. A hint on only one path is how a bare
734
- * `type "vector" does not exist` reaches somebody — which is the thing this
735
- * exists to prevent.
736
- */
737
- var vectorExtensionHint = (message) => {
738
- if (/extension "vector" is not available/i.test(message) || /(permission denied to create extension|must be (superuser|owner).{0,40}extension)/i.test(message)) return "\n pgvector was declared and could not be installed. It is a server extension, so it needs an image that ships the library (the scaffold's `pgvector/pgvector:pg18` does; a stock `postgres:18` does not) and a role allowed to run `CREATE EXTENSION vector;`. Managed Postgres usually allows it once the extension is on the provider's allow-list.";
739
- if (!/type "(vector|halfvec|sparsevec)" does not exist/i.test(message)) return "";
740
- return `
741
- pgvector is not installed on this database, and Rebase installs it only where a database says it may: add \`${VECTOR_EXTENSION_OPT_IN}\` in config/resources.ts, or install it once by hand with \`CREATE EXTENSION vector;\`. Either way the server needs an image that ships the library — the scaffold's \`pgvector/pgvector:pg18\` does, a stock \`postgres:18\` does not. Rebase then creates the column and its ANN index automatically — see the \`index\` option on the property.`;
742
- };
743
- /**
744
- * Every vector column a collection declares.
745
- *
746
- * Wider than {@link buildVectorIndexPlan} on purpose: that one answers "what
747
- * gets an ANN index", and skips both a property with `index: false` and one too
748
- * wide to index. Either still needs its column.
749
- */
750
- var buildVectorColumnSpecs = (collection, resolveColumn) => {
751
- const table = getTableName(collection);
752
- const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
753
- const specs = [];
754
- for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
755
- if (!isVectorProperty(prop)) continue;
756
- const validation = prop.validation;
757
- let modifiers = "";
758
- if (validation?.unique) modifiers += " UNIQUE";
759
- if (validation?.required) modifiers += " NOT NULL";
760
- specs.push({
761
- schema,
762
- table,
763
- column: resolveColumn(propName, prop),
764
- dimensions: prop.dimensions,
765
- modifiers
766
- });
767
- }
768
- return specs;
769
- };
770
- /** The column type — the one place `VECTOR(n)` is spelled. */
771
- var vectorColumnType = (spec) => `VECTOR(${spec.dimensions})`;
772
- /** The column definition as it appears inside `CREATE TABLE`. */
773
- var vectorColumnDefinition = (spec) => `"${spec.column}" ${vectorColumnType(spec)}${spec.modifiers}`;
774
- /**
775
- * Refuse — or perform — a `dimensions` change that `ADD COLUMN IF NOT EXISTS`
776
- * would otherwise swallow.
777
- *
778
- * Without this the file *launders* the change: the ADD is a no-op against a
779
- * column that exists, so a project that went from 384 to 768 dimensions would
780
- * push clean, keep a 384-wide column, and fail on the next insert with a
781
- * message about the row rather than about the config. Atlas used to catch this
782
- * — it owned the column and planned the `ALTER … TYPE` — and taking the column
783
- * out of its sight is exactly what makes the guard necessary.
784
- *
785
- * The widening is performed when the column holds no values, because that is
786
- * the case Atlas handled and it is the common one: a developer changing
787
- * embedding models before there are any embeddings. With values present the
788
- * conversion is pgvector's to reject — every stored vector is the old width —
789
- * so this refuses first and names the statement to run afterwards.
790
- *
791
- * `atttypmod` on a `vector` column *is* the dimension count: pgvector stores it
792
- * directly rather than offsetting it the way `varchar` does. `-1` means the
793
- * column was declared as a bare `vector`, which is drift in the same sense.
794
- */
795
- var vectorDimensionGuard = (spec) => {
796
- const relation = `"${spec.schema}"."${spec.table}"`;
797
- return `DO $rebase_vector$
798
- DECLARE actual int;
799
- BEGIN
800
- SELECT a.atttypmod INTO actual
801
- FROM pg_attribute a
802
- JOIN pg_type t ON t.oid = a.atttypid
803
- WHERE a.attrelid = ${quote(relation)}::regclass
804
- AND a.attname = ${quote(spec.column)}
805
- AND NOT a.attisdropped
806
- AND t.typname = 'vector';
807
- IF actual IS NOT NULL AND actual <> ${spec.dimensions} THEN
808
- IF EXISTS (SELECT 1 FROM ${relation} WHERE "${spec.column}" IS NOT NULL) THEN
809
- RAISE EXCEPTION 'Rebase: ${spec.schema}.${spec.table}."${spec.column}" declares ${spec.dimensions} dimensions but the column is vector(%), and it already holds values of the old width. pgvector cannot convert them. Re-embed the rows at ${spec.dimensions} dimensions (or clear the column), then re-apply: ALTER TABLE ${relation} ALTER COLUMN "${spec.column}" TYPE ${vectorColumnType(spec)};', actual;
810
- END IF;
811
- ALTER TABLE ${relation} ALTER COLUMN "${spec.column}" TYPE ${vectorColumnType(spec)};
812
- END IF;
813
- END
814
- $rebase_vector$;`;
815
- };
816
- //#endregion
817
431
  //#region src/schema/generate-postgres-ddl-logic.ts
432
+ var generate_postgres_ddl_logic_exports = /* @__PURE__ */ __exportAll({
433
+ defaultBelongsToOnDelete: () => defaultBelongsToOnDelete,
434
+ generatePolicyStatements: () => generatePolicyStatements,
435
+ generatePostgresDdl: () => generatePostgresDdl,
436
+ generatePostgresPoliciesDdl: () => generatePostgresPoliciesDdl,
437
+ generatePostgresSearchDdl: () => generatePostgresSearchDdl,
438
+ generatePostgresVectorDdl: () => generatePostgresVectorDdl,
439
+ getPrimaryKeyName: () => getPrimaryKeyName,
440
+ getPrimaryKeyProp: () => getPrimaryKeyProp,
441
+ getSqlColumnType: () => getSqlColumnType,
442
+ isIdProperty: () => isIdProperty,
443
+ isNumericId: () => isNumericId,
444
+ planCollectionPolicies: () => planCollectionPolicies,
445
+ planJunctionTables: () => planJunctionTables,
446
+ planRelationalColumns: () => planRelationalColumns,
447
+ quoteSqlLiteral: () => quoteSqlLiteral,
448
+ resolveColumnName: () => resolveColumnName,
449
+ searchExcludePatterns: () => searchExcludePatterns,
450
+ vectorExcludePatterns: () => vectorExcludePatterns
451
+ });
818
452
  var resolveColumnName = (propName, prop) => {
819
453
  if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
820
454
  return toSnakeCase(propName);
@@ -864,6 +498,34 @@ var isIdProperty = (propName, prop, collection) => {
864
498
  * the generated `policies.sql` emit the same SQL, from the same source.
865
499
  */
866
500
  var statementsToDdl = (statements) => statements.map((s) => `${s}\n`).join("");
501
+ /**
502
+ * The `ON DELETE` a foreign key gets when the author did not say.
503
+ *
504
+ * One rule for every property that emits one — `belongsTo` relations and
505
+ * `reference` properties alike. `reference` kept its own `CASCADE` default for
506
+ * a release after this rule was written, which is exactly the split this
507
+ * function exists to prevent: two spellings of the same link, two data-retention
508
+ * behaviours, and only one of them documented.
509
+ *
510
+ * An optional link is `SET NULL`: the column can hold NULL, so dropping the
511
+ * parent leaves the child row with an empty pointer, which is what "optional"
512
+ * already means.
513
+ *
514
+ * A required link is **`RESTRICT`**, not `CASCADE`. `NOT NULL` says the child
515
+ * cannot exist without a parent; it does not say deleting the parent should
516
+ * take the child with it. That second claim is a data-retention decision, and
517
+ * defaulting to it meant `onDelete` — a field nobody has to write — silently
518
+ * turned every `DELETE FROM authors` into a cascade through posts, comments and
519
+ * anything else that hung off them. `RESTRICT` fails the delete instead and
520
+ * names the constraint, so the author chooses `cascade` on purpose or clears
521
+ * the children first.
522
+ *
523
+ * Both DDL paths — the `CREATE TABLE` generator and {@link planRelationalColumns},
524
+ * which boot-ensure compares the live database against — call this, so a
525
+ * default that appears in one and not the other cannot make `db push` plan a
526
+ * constraint rewrite on every run.
527
+ */
528
+ var defaultBelongsToOnDelete = (required) => required ? "RESTRICT" : "SET NULL";
867
529
  var generatePolicyDdl = (collection, rule, resolveCollection) => statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));
868
530
  /**
869
531
  * The individual SQL statements a single security rule compiles to: a
@@ -1058,6 +720,31 @@ var generatePostgresSearchDdl = (allCollections) => {
1058
720
  return ddl;
1059
721
  };
1060
722
  /**
723
+ * Glob patterns telling Atlas to leave the search column and its index alone.
724
+ *
725
+ * Without these, a desired state that omits search reads to Atlas as an
726
+ * instruction to drop the column — taking the index and the whole search
727
+ * feature with it on the next push.
728
+ *
729
+ * Fully qualified, `schema.table.object`, matching the include list. The
730
+ * two-part form is what Atlas wants when the connection URL scopes it to one
731
+ * schema and is *silently ignored* otherwise: it reads `posts.search_vector`
732
+ * as a table named `search_vector` in a schema named `posts`, matches nothing,
733
+ * and reports no error for the pattern that never fired.
734
+ */
735
+ var searchExcludePatterns = (allCollections) => {
736
+ const patterns = [];
737
+ for (const collection of relationalCollections(allCollections)) {
738
+ const spec = buildSearchColumnSpec(collection);
739
+ if (!spec) continue;
740
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
741
+ const table = getTableName(collection);
742
+ for (const name of searchColumnNames(collection)) patterns.push(`${schema}.${table}.${name}`);
743
+ for (const index of searchIndexNames(spec)) patterns.push(`${schema}.${table}.${index}`);
744
+ }
745
+ return patterns;
746
+ };
747
+ /**
1061
748
  * Everything a `{ type: "vector" }` property needs, as a file Rebase applies
1062
749
  * itself.
1063
750
  *
@@ -1113,6 +800,27 @@ var generatePostgresVectorDdl = (allCollections, options = {}) => {
1113
800
  }
1114
801
  return ddl;
1115
802
  };
803
+ /**
804
+ * Glob patterns telling Atlas to leave the vector column and its ANN indexes
805
+ * alone.
806
+ *
807
+ * Without these, a desired state that omits the column reads to Atlas as an
808
+ * instruction to drop it, taking the embeddings and the indexes with it on the
809
+ * next push.
810
+ *
811
+ * Fully qualified, `schema.table.object`, for the reason spelled out on
812
+ * {@link searchExcludePatterns}: the two-part form matches nothing and reports
813
+ * no error, which turns a protection into a `DROP COLUMN`.
814
+ */
815
+ var vectorExcludePatterns = (allCollections) => {
816
+ const patterns = [];
817
+ for (const collection of relationalCollections(allCollections)) {
818
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
819
+ const table = getTableName(collection);
820
+ for (const name of vectorObjectNames(collection, resolveColumnName)) patterns.push(`${schema}.${table}.${name}`);
821
+ }
822
+ return patterns;
823
+ };
1116
824
  var generatePostgresDdl = async (allCollections, options = {
1117
825
  includePolicies: true,
1118
826
  includeSearch: true,
@@ -1234,7 +942,7 @@ var generatePostgresDdl = async (allCollections, options = {
1234
942
  const fkColType = getSqlColumnType(propName, prop, collection, collections);
1235
943
  const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : "";
1236
944
  const required = prop.validation?.required;
1237
- const onDeleteVal = relInfo.onDelete ?? (required ? "CASCADE" : "SET NULL");
945
+ const onDeleteVal = relInfo.onDelete ?? defaultBelongsToOnDelete(required);
1238
946
  let colDef = ` "${relInfo.localKey}" ${fkColType}`;
1239
947
  if (required) colDef += " NOT NULL";
1240
948
  columns.push(colDef);
@@ -1253,7 +961,7 @@ var generatePostgresDdl = async (allCollections, options = {
1253
961
  const targetTable = getTableName(targetCollection);
1254
962
  const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
1255
963
  const targetId = getPrimaryKeyName(targetCollection);
1256
- const onDelete = required ? "CASCADE" : "SET NULL";
964
+ const onDelete = defaultBelongsToOnDelete(required);
1257
965
  let colDef = ` "${colName}" ${colType}`;
1258
966
  if (required) colDef += " NOT NULL";
1259
967
  columns.push(colDef);
@@ -1420,7 +1128,7 @@ var planRelationalColumns = (allCollections) => {
1420
1128
  targetSchema: schemaOfCollection(targetCollection),
1421
1129
  targetTable: bareTableName(getTableName(targetCollection)),
1422
1130
  targetColumn: getPrimaryKeyName(targetCollection),
1423
- onDelete: relInfo.onDelete ?? (required ? "CASCADE" : "SET NULL"),
1131
+ onDelete: relInfo.onDelete ?? defaultBelongsToOnDelete(required),
1424
1132
  onUpdate: relInfo.onUpdate
1425
1133
  })
1426
1134
  });
@@ -1442,7 +1150,7 @@ var planRelationalColumns = (allCollections) => {
1442
1150
  targetSchema: schemaOfCollection(targetCollection),
1443
1151
  targetTable: bareTableName(getTableName(targetCollection)),
1444
1152
  targetColumn: getPrimaryKeyName(targetCollection),
1445
- onDelete: required ? "CASCADE" : "SET NULL"
1153
+ onDelete: defaultBelongsToOnDelete(required)
1446
1154
  }) : void 0
1447
1155
  });
1448
1156
  }
@@ -1599,572 +1307,6 @@ var generatePostgresPoliciesDdl = (allCollections) => {
1599
1307
  return ddl;
1600
1308
  };
1601
1309
  //#endregion
1602
- //#region src/schema/ensure-collection-tables.ts
1603
- /**
1604
- * Bringing a database up to date with a bundle's collections, additively.
1605
- *
1606
- * ## Why this exists
1607
- *
1608
- * A managed runtime boots someone else's compiled project against a database it
1609
- * has never seen. Auth tables are ensured at boot already, but collection tables
1610
- * were not created by anything: the platform ran the app and every `/api/data/*`
1611
- * request answered 500 on a missing relation. `rebase db push` cannot help — it
1612
- * is an Atlas-driven CLI command, and the runtime image ships no CLI.
1613
- *
1614
- * ## Why additive-only, forever
1615
- *
1616
- * This runs unattended, against a database with customers' data in it, with no
1617
- * human reading a diff. So it may only ever do things that cannot lose data:
1618
- * create a missing table, add a missing column, create a missing enum type.
1619
- *
1620
- * It will **never** drop a table or a column, narrow a type, or alter a
1621
- * constraint. A removed field leaves its column behind; a renamed field looks
1622
- * like an addition and the old column stays. That is the correct trade for an
1623
- * automated path — the alternative is an unattended process that can silently
1624
- * destroy a column, which is precisely the failure `db push` was hardened
1625
- * against. Destructive changes stay a deliberate, human-reviewed migration.
1626
- *
1627
- * Because of that, this is safe to run on every boot, and re-running it is a
1628
- * no-op.
1629
- */
1630
- var ensure_collection_tables_exports = /* @__PURE__ */ __exportAll({
1631
- ensureCollectionTables: () => ensureCollectionTables,
1632
- planCollectionSchemaEnsure: () => planCollectionSchemaEnsure,
1633
- readExistingSchema: () => readExistingSchema,
1634
- readSchemaFactsFor: () => readSchemaFactsFor
1635
- });
1636
- /** Postgres identifiers this module is willing to interpolate. */
1637
- var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1638
- function assertSafeIdentifier(value, what) {
1639
- if (!SAFE_IDENTIFIER.test(value)) throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);
1640
- return value;
1641
- }
1642
- function schemaOf(collection) {
1643
- return isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
1644
- }
1645
- function qualified(collection) {
1646
- return `${schemaOf(collection)}.${getTableName(collection)}`;
1647
- }
1648
- /**
1649
- * Enum types a collection's properties require, as `schema.typename`.
1650
- *
1651
- * Named exactly as the DDL generator names them (`<table>_<column>`), because
1652
- * a column added here has to reference the same type the generator would have
1653
- * created — a second, differently-named type for the same field would be a
1654
- * silent schema fork.
1655
- */
1656
- function requiredEnums(collection) {
1657
- const table = getTableName(collection);
1658
- const schema = schemaOf(collection);
1659
- const out = [];
1660
- for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
1661
- const p = prop;
1662
- if (!("enum" in p) || !p.enum) continue;
1663
- if (p.type !== "string" && p.type !== "number") continue;
1664
- const values = p.enum.map((entry) => entry && typeof entry === "object" && "id" in entry ? String(entry.id) : String(entry)).filter((v) => v.length > 0);
1665
- if (values.length === 0) continue;
1666
- out.push({
1667
- name: `${schema}.${table}_${resolveColumnName(propName, p)}`,
1668
- values
1669
- });
1670
- }
1671
- return out;
1672
- }
1673
- /**
1674
- * Decide what to add. Pure — the caller supplies what exists and runs the result.
1675
- *
1676
- * Ordering matters and is deliberate: enum types before the tables and columns
1677
- * that reference them, tables before the columns added to other tables (a new
1678
- * table may be the target of a relation), and nothing is emitted twice.
1679
- */
1680
- function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
1681
- const constraintPolicy = options.constraints ?? "additive";
1682
- const withheldConstraints = [];
1683
- assertSearchIsPostgresOnly(allCollections);
1684
- const collections = relationalCollections(allCollections);
1685
- const actions = [];
1686
- const plannedEnums = /* @__PURE__ */ new Set();
1687
- for (const collection of collections) for (const { name, values } of requiredEnums(collection)) {
1688
- if (existing.enums.has(name) || plannedEnums.has(name)) {
1689
- const current = existing.enumValues?.get(name);
1690
- if (!current || plannedEnums.has(name)) continue;
1691
- const [schema, typeName] = name.split(".");
1692
- for (const value of values) {
1693
- if (current.includes(value)) continue;
1694
- actions.push({
1695
- kind: "add-enum-value",
1696
- target: `${name}.${value}`,
1697
- sql: `ALTER TYPE "${schema}"."${typeName}" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`
1698
- });
1699
- }
1700
- continue;
1701
- }
1702
- plannedEnums.add(name);
1703
- const [schema, typeName] = name.split(".");
1704
- actions.push({
1705
- kind: "create-enum",
1706
- target: name,
1707
- sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});`
1708
- });
1709
- }
1710
- const searchSpecs = collections.map((c) => buildSearchColumnSpec(c)).filter((spec) => spec !== void 0);
1711
- const plannedExtensions = /* @__PURE__ */ new Set();
1712
- const planExtension = (statement) => {
1713
- if (plannedExtensions.has(statement)) return;
1714
- plannedExtensions.add(statement);
1715
- actions.push({
1716
- kind: "create-extension",
1717
- target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""),
1718
- sql: statement
1719
- });
1720
- };
1721
- for (const spec of searchSpecs) for (const statement of searchExtensionStatements(spec)) planExtension(statement);
1722
- if (vectorExtensionDeclared(options.databaseExtensions) && collections.some((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0)) planExtension(vectorExtensionStatement());
1723
- const plannedFunctions = /* @__PURE__ */ new Set();
1724
- for (const spec of searchSpecs) for (const statement of searchHelperFunctions(spec)) {
1725
- if (plannedFunctions.has(statement)) continue;
1726
- plannedFunctions.add(statement);
1727
- actions.push({
1728
- kind: "create-function",
1729
- target: statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN,
1730
- sql: statement
1731
- });
1732
- }
1733
- const created = /* @__PURE__ */ new Set();
1734
- for (const collection of collections) {
1735
- const key = qualified(collection);
1736
- if (existing.tables.has(key) || created.has(key)) continue;
1737
- created.add(key);
1738
- const schema = schemaOf(collection);
1739
- const table = getTableName(collection);
1740
- const idEntry = Object.entries(collection.properties ?? {}).find(([n, p]) => isIdProperty(n, p, collection));
1741
- const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1]) : "id";
1742
- const idProp = idEntry?.[1];
1743
- let idDef = `"${idName}" ${idProp ? getSqlColumnType(idEntry[0], idProp, collection, collections) : "TEXT"} PRIMARY KEY`;
1744
- if (idProp?.type === "string" && idProp.isId === "uuid") idDef += " DEFAULT gen_random_uuid()";
1745
- actions.push({
1746
- kind: "create-table",
1747
- target: key,
1748
- sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${table}" (${idDef});`
1749
- });
1750
- }
1751
- const junctions = planJunctionTables(collections);
1752
- for (const junction of junctions) {
1753
- const key = `${junction.schema}.${junction.table}`;
1754
- if (existing.tables.has(key) || created.has(key)) continue;
1755
- created.add(key);
1756
- actions.push({
1757
- kind: "create-table",
1758
- target: key,
1759
- sql: junction.createTable
1760
- });
1761
- }
1762
- const legacyForeignKeys = [];
1763
- /**
1764
- * Move a relation column that is only missing because it was renamed.
1765
- *
1766
- * Returns true when it handled the column, so the caller skips the ordinary
1767
- * ADD. Adding here would be the wrong move and a quiet one: the data is in
1768
- * the old column, `ADD COLUMN` creates the new one empty beside it, every
1769
- * statement succeeds, and the relation reads the empty one. A rename is
1770
- * metadata-only in Postgres, keeps the values, and carries the column's
1771
- * indexes and constraints with it.
1772
- *
1773
- * Only ever reached when the new name is absent and the old name is
1774
- * present, so there is nothing to overwrite and nothing to choose between.
1775
- */
1776
- const renameLegacyColumn = (key, schema, table, column, legacyName) => {
1777
- const present = existing.tables.get(key);
1778
- if (!legacyName || !present) return false;
1779
- if (present.has(column) || !present.has(legacyName)) return false;
1780
- legacyForeignKeys.push({
1781
- table: key,
1782
- expected: column,
1783
- legacy: legacyName
1784
- });
1785
- actions.push({
1786
- kind: "rename-column",
1787
- target: `${key}.${column}`,
1788
- sql: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${legacyName}" TO "${column}";`
1789
- });
1790
- return true;
1791
- };
1792
- const addColumn = (key, schema, table, column, definition) => {
1793
- if (existing.tables.get(key)?.has(column)) return;
1794
- actions.push({
1795
- kind: "add-column",
1796
- target: `${key}.${column}`,
1797
- sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${definition};`
1798
- });
1799
- };
1800
- for (const collection of collections) {
1801
- const key = qualified(collection);
1802
- const schema = schemaOf(collection);
1803
- const table = getTableName(collection);
1804
- const fresh = created.has(key);
1805
- const auth = isAuthCollection(collection);
1806
- for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
1807
- const p = prop;
1808
- if (isIdProperty(propName, p, collection)) continue;
1809
- if (p.type === "reference" || p.type === "relation") continue;
1810
- const column = resolveColumnName(propName, p);
1811
- const authDefinition = auth ? authUsersColumnDefinition(column) : void 0;
1812
- if (authDefinition) {
1813
- addColumn(key, schema, table, column, authDefinition);
1814
- continue;
1815
- }
1816
- let definition = getSqlColumnType(propName, p, collection, collections);
1817
- if (fresh && p.validation?.unique) definition += " UNIQUE";
1818
- const autoValue = p.autoValue;
1819
- const hasDefault = p.type === "date" && (autoValue === "on_create" || autoValue === "on_update");
1820
- if (hasDefault) definition += " DEFAULT now()";
1821
- const required = p.validation?.required === true;
1822
- const columnKey = `${key}.${column}`;
1823
- const columnExists = existing.tables.get(key)?.has(column) === true;
1824
- const tableIsEmpty = existing.populatedTables !== void 0 && existing.tables.has(key) && !existing.populatedTables.has(key);
1825
- const notNullIsSafe = fresh || tableIsEmpty || hasDefault;
1826
- if (required && !columnExists) if (notNullIsSafe) definition += " NOT NULL";
1827
- else withheldConstraints.push({
1828
- target: columnKey,
1829
- kind: "not-null",
1830
- 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.`,
1831
- remedy: "Backfill the column, then add the constraint — or give the property a default so every existing row gets one."
1832
- });
1833
- addColumn(key, schema, table, column, definition);
1834
- if (columnExists && constraintPolicy === "converge") {
1835
- const isNotNull = existing.notNullColumns?.has(columnKey) === true;
1836
- if (required && !isNotNull) if (tableIsEmpty) actions.push({
1837
- kind: "set-not-null",
1838
- target: columnKey,
1839
- sql: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column}" SET NOT NULL;`
1840
- });
1841
- else withheldConstraints.push({
1842
- target: columnKey,
1843
- kind: "not-null",
1844
- reason: `"${column}" became required, but "${key}" holds rows and any of them with no value would make SET NOT NULL fail.`,
1845
- remedy: "Backfill the column first — `UPDATE … SET \"" + column + "\" = … WHERE \"" + column + "\" IS NULL` — then apply this again."
1846
- });
1847
- if (!required && isNotNull) actions.push({
1848
- kind: "drop-not-null",
1849
- target: columnKey,
1850
- sql: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column}" DROP NOT NULL;`
1851
- });
1852
- }
1853
- }
1854
- if (auth) {
1855
- const declared = new Set(Object.entries(collection.properties ?? {}).map(([name, prop]) => resolveColumnName(name, prop)));
1856
- for (const spec of AUTH_USERS_COLUMNS) {
1857
- if (declared.has(spec.column)) continue;
1858
- addColumn(key, schema, table, spec.column, authUsersColumnSql(spec));
1859
- }
1860
- }
1861
- }
1862
- const searchDrift = [];
1863
- const searchAdopted = [];
1864
- for (const spec of searchSpecs) {
1865
- const key = `${spec.schema}.${spec.table}`;
1866
- const definitions = { [spec.column]: `tsvector GENERATED ALWAYS AS (${spec.expression}) STORED` };
1867
- if (spec.fuzzy) definitions[spec.fuzzy.column] = `text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED`;
1868
- for (const stamp of searchColumnStamps(spec)) {
1869
- const definition = definitions[stamp.column];
1870
- const exists = existing.tables.get(key)?.has(stamp.column) === true;
1871
- const recorded = existing.columnComments?.get(`${key}.${stamp.column}`);
1872
- if (exists && recorded?.startsWith("rebase:search:v1:") && recorded !== stamp.fingerprint) {
1873
- searchDrift.push({
1874
- table: key,
1875
- column: stamp.column,
1876
- found: recorded,
1877
- expected: stamp.fingerprint,
1878
- rebuild: [
1879
- `ALTER TABLE "${spec.schema}"."${spec.table}" DROP COLUMN "${stamp.column}";`,
1880
- `ALTER TABLE "${spec.schema}"."${spec.table}" ADD COLUMN "${stamp.column}" ${definition};`,
1881
- stamp.sql
1882
- ]
1883
- });
1884
- continue;
1885
- }
1886
- addColumn(key, spec.schema, spec.table, stamp.column, definition);
1887
- if (exists && recorded === void 0) searchAdopted.push({
1888
- table: key,
1889
- column: stamp.column
1890
- });
1891
- if (recorded !== stamp.fingerprint) actions.push({
1892
- kind: "comment-column",
1893
- target: `${key}.${stamp.column}`,
1894
- sql: stamp.sql
1895
- });
1896
- }
1897
- }
1898
- for (const junction of junctions) {
1899
- const key = `${junction.schema}.${junction.table}`;
1900
- if (created.has(key)) continue;
1901
- for (const column of junction.columns) {
1902
- if (renameLegacyColumn(key, junction.schema, junction.table, column.name, column.legacyName)) continue;
1903
- addColumn(key, junction.schema, junction.table, column.name, column.type);
1904
- }
1905
- }
1906
- for (const relational of planRelationalColumns(collections)) {
1907
- const relKey = `${relational.schema}.${relational.table}`;
1908
- if (renameLegacyColumn(relKey, relational.schema, relational.table, relational.column, relational.legacyColumn)) continue;
1909
- addColumn(relKey, relational.schema, relational.table, relational.column, relational.type);
1910
- }
1911
- const knownConstraints = existing.constraints ?? /* @__PURE__ */ new Set();
1912
- const plannedConstraints = /* @__PURE__ */ new Set();
1913
- const foreignKeys = [...planRelationalColumns(collections).map((r) => r.foreignKey), ...junctions.flatMap((j) => j.foreignKeys)];
1914
- for (const fk of foreignKeys) {
1915
- if (!fk) continue;
1916
- const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;
1917
- if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;
1918
- plannedConstraints.add(name);
1919
- actions.push({
1920
- kind: "add-constraint",
1921
- target: `${fk.schema}.${fk.table}.${fk.constraintName}`,
1922
- sql: fk.sql
1923
- });
1924
- }
1925
- for (const spec of searchSpecs) for (const statement of searchIndexStatements(spec)) actions.push({
1926
- kind: "create-index",
1927
- target: `${spec.schema}.${spec.table}`,
1928
- sql: statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS")
1929
- });
1930
- const vectorIndexSkipped = [];
1931
- for (const collection of collections) {
1932
- const plan = buildVectorIndexPlan(collection, resolveColumnName);
1933
- for (const spec of plan.specs) actions.push({
1934
- kind: "create-index",
1935
- target: `${spec.schema}.${spec.table}`,
1936
- sql: vectorIndexStatement(spec).replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS")
1937
- });
1938
- vectorIndexSkipped.push(...plan.skipped);
1939
- }
1940
- for (const spec of buildCollectionIndexPlan(collections, resolveColumnName)) actions.push({
1941
- kind: "create-index",
1942
- target: `${spec.schema}.${spec.table}`,
1943
- sql: collectionIndexStatement(spec, {
1944
- concurrently: true,
1945
- ifNotExists: true
1946
- })
1947
- });
1948
- return {
1949
- actions,
1950
- statements: actions.map((a) => a.sql),
1951
- legacyForeignKeys,
1952
- searchDrift,
1953
- searchAdopted,
1954
- vectorIndexSkipped,
1955
- withheldConstraints
1956
- };
1957
- }
1958
- /** Read what the database has, for the schemas the collections live in. */
1959
- async function readExistingSchema(client, schemas) {
1960
- const tables = /* @__PURE__ */ new Map();
1961
- const enums = /* @__PURE__ */ new Set();
1962
- if (schemas.length === 0) return {
1963
- tables,
1964
- enums
1965
- };
1966
- const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
1967
- const notNullColumns = /* @__PURE__ */ new Set();
1968
- const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable
1969
- FROM information_schema.columns
1970
- WHERE table_schema IN (${inList})`);
1971
- for (const row of columns) {
1972
- const key = `${row.table_schema}.${row.table_name}`;
1973
- if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
1974
- tables.get(key).add(row.column_name);
1975
- if (row.is_nullable === "NO") notNullColumns.add(`${key}.${row.column_name}`);
1976
- }
1977
- const populatedTables = /* @__PURE__ */ new Set();
1978
- const { rows: realTables } = await client.query(`SELECT n.nspname AS schema, c.relname AS name
1979
- FROM pg_class c
1980
- JOIN pg_namespace n ON c.relnamespace = n.oid
1981
- WHERE c.relkind IN ('r', 'p') AND n.nspname IN (${inList})`);
1982
- if (realTables.length > 0) {
1983
- const probes = realTables.map((row) => {
1984
- const schema = assertSafeIdentifier(row.schema, "schema name");
1985
- const table = assertSafeIdentifier(row.name, "table name");
1986
- return `SELECT ${quoteSqlLiteral(`${schema}.${table}`)} AS key, EXISTS(SELECT 1 FROM "${schema}"."${table}" LIMIT 1) AS populated`;
1987
- });
1988
- const { rows: populationRows } = await client.query(probes.join(" UNION ALL "));
1989
- for (const row of populationRows) if (row.populated) populatedTables.add(row.key);
1990
- }
1991
- const enumValues = /* @__PURE__ */ new Map();
1992
- const { rows: enumValueRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value
1993
- FROM pg_enum e
1994
- JOIN pg_type t ON e.enumtypid = t.oid
1995
- JOIN pg_namespace n ON t.typnamespace = n.oid
1996
- WHERE n.nspname IN (${inList})
1997
- ORDER BY t.typname, e.enumsortorder`);
1998
- for (const row of enumValueRows) {
1999
- const key = `${row.schema}.${row.name}`;
2000
- if (!enumValues.has(key)) enumValues.set(key, []);
2001
- enumValues.get(key).push(row.value);
2002
- }
2003
- const { rows: enumRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name
2004
- FROM pg_type t
2005
- JOIN pg_namespace n ON t.typnamespace = n.oid
2006
- WHERE t.typtype = 'e' AND n.nspname IN (${inList})`);
2007
- for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
2008
- const constraints = /* @__PURE__ */ new Set();
2009
- const { rows: constraintRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, con.conname AS name
2010
- FROM pg_constraint con
2011
- JOIN pg_class c ON con.conrelid = c.oid
2012
- JOIN pg_namespace n ON c.relnamespace = n.oid
2013
- WHERE n.nspname IN (${inList})`);
2014
- for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
2015
- const columnComments = /* @__PURE__ */ new Map();
2016
- const { rows: commentRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment
2017
- FROM pg_description d
2018
- JOIN pg_class c ON d.objoid = c.oid
2019
- JOIN pg_namespace n ON c.relnamespace = n.oid
2020
- JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid
2021
- WHERE d.objsubid > 0 AND n.nspname IN (${inList})`);
2022
- for (const row of commentRows) {
2023
- if (row.comment == null) continue;
2024
- columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);
2025
- }
2026
- return {
2027
- tables,
2028
- enums,
2029
- constraints,
2030
- columnComments,
2031
- enumValues,
2032
- notNullColumns,
2033
- populatedTables
2034
- };
2035
- }
2036
- /**
2037
- * What to tell an operator whose `search` block no longer matches its column.
2038
- *
2039
- * Every line here is doing work: naming the collection is not enough, because
2040
- * the symptom (a search that finds nothing) points at the data, not the schema;
2041
- * and the remediation has to be exact, because it is a table rewrite the
2042
- * operator is being asked to schedule rather than discover.
2043
- */
2044
- function searchDriftMessage(drift) {
2045
- return "The `search` block changed after its generated column was created, and Postgres cannot alter a generated expression in place.\nRebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path may not schedule on your behalf.\nUntil it is rebuilt the column keeps indexing the previous fields, weights and language — searches for anything added since return nothing, which reads from outside as \"no such row\".\nRun these (or revert the block to what the column was built from), then boot again:\n" + drift.map((d) => ` "${d.table}"."${d.column}" was generated from a different \`search\` block (recorded ${d.found}, current ${d.expected}).\n` + d.rebuild.map((s) => ` ${s}`).join("\n")).join("\n") + "\n The GIN index is dropped with the column and recreated concurrently on the next boot.";
2046
- }
2047
- /**
2048
- * Read what the database looks like, for the schemas a set of collections
2049
- * lives in.
2050
- *
2051
- * The same read `ensureCollectionTables` does at boot, exposed on its own for
2052
- * the callers that want to *plan* against a real database without changing it —
2053
- * the live schema editor, which has to tell somebody what a change would do
2054
- * before they agree to it.
2055
- */
2056
- async function readSchemaFactsFor(client, collections) {
2057
- const relational = relationalCollections(collections);
2058
- return readExistingSchema(client, Array.from(/* @__PURE__ */ new Set([...relational.map(schemaOf), ...planJunctionTables(relational).map((junction) => junction.schema)])));
2059
- }
2060
- /**
2061
- * Bring the database up to date. Returns what it did.
2062
- *
2063
- * Each statement runs on its own rather than in one transaction: they are all
2064
- * independently safe and idempotent, and a single failure (an enum label that
2065
- * cannot be added, say) should not roll back the tables that were created fine.
2066
- * The error is surfaced with the statement that caused it.
2067
- */
2068
- async function ensureCollectionTables(client, collections, log, options = {}) {
2069
- const schemas = Array.from(/* @__PURE__ */ new Set([...collections.map(schemaOf), ...planJunctionTables(collections).map((j) => j.schema)]));
2070
- for (const schema of schemas) {
2071
- assertSafeIdentifier(schema, "schema name");
2072
- if (schema !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}";`);
2073
- }
2074
- const plan = planCollectionSchemaEnsure(collections, await readExistingSchema(client, schemas), {
2075
- ...options,
2076
- databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()
2077
- });
2078
- const failures = [];
2079
- for (const legacy of plan.legacyForeignKeys) {
2080
- 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.`;
2081
- logger.info(`[schema] ${message}`);
2082
- log?.(message);
2083
- }
2084
- if (plan.searchDrift.length > 0) throw new Error(searchDriftMessage(plan.searchDrift));
2085
- for (const adopted of plan.searchAdopted) {
2086
- const message = `Adopting the existing generated column "${adopted.table}"."${adopted.column}" and recording what the current \`search\` block would generate. Any later change to that block will be detected and refused; a change made *before* this version was deployed cannot be, so if search has been missing content, rebuild the column once: ALTER TABLE "${adopted.table.split(".").join("\".\"")}" DROP COLUMN "${adopted.column}"; and boot again.`;
2087
- logger.info(`[schema] ${message}`);
2088
- log?.(message);
2089
- }
2090
- for (const skip of plan.vectorIndexSkipped) {
2091
- const message = `No ANN index on "${skip.table}"."${skip.column}": ${skip.reason}`;
2092
- logger.warn(`[schema] ${message}`);
2093
- log?.(message);
2094
- }
2095
- for (const withheld of plan.withheldConstraints) {
2096
- const message = `No NOT NULL on "${withheld.target}": ${withheld.reason} ${withheld.remedy}`;
2097
- logger.warn(`[schema] ${message}`);
2098
- log?.(message);
2099
- }
2100
- if (plan.actions.length === 0) {
2101
- log?.("Schema is up to date; nothing to create.");
2102
- return {
2103
- ...plan,
2104
- failures
2105
- };
2106
- }
2107
- for (const action of plan.actions) try {
2108
- if (await applyAction(client, action)) log?.(`${action.kind}: ${action.target}`);
2109
- else log?.(`${action.kind}: ${action.target} (already created by a peer)`);
2110
- } catch (err) {
2111
- const message = err instanceof Error ? err.message : String(err);
2112
- if (action.kind === "add-constraint" || action.kind === "comment-column") {
2113
- failures.push({
2114
- kind: action.kind,
2115
- target: action.target,
2116
- error: message
2117
- });
2118
- continue;
2119
- }
2120
- throw new Error(`Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\n ${action.sql}`);
2121
- }
2122
- return {
2123
- ...plan,
2124
- failures
2125
- };
2126
- }
2127
- /** Attempts per action, including the first. Matches the server's bootstraps. */
2128
- var DDL_ATTEMPTS = 4;
2129
- /**
2130
- * Run one planned statement, surviving a simultaneous boot.
2131
- *
2132
- * Every statement in a plan is written to be idempotent, and that is not the
2133
- * same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the
2134
- * catalog and then writes to it as two steps, so peers starting together both
2135
- * see "absent" and the loser gets a duplicate key on a *catalog* index. Measured
2136
- * against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is
2137
- * worse, because Postgres has no `IF NOT EXISTS` for it at all.
2138
- *
2139
- * What made that fatal here rather than merely noisy is the loop this sits in.
2140
- * A losing statement threw, and the throw abandoned **every remaining action in
2141
- * the plan** — so a replica that lost one race came up missing tables it never
2142
- * attempted, and the boot log blamed the one statement that failed.
2143
- *
2144
- * @returns `true` if this process applied the statement, `false` if a peer had
2145
- * already created the object. The distinction is only for the log; both mean
2146
- * the object is now there.
2147
- * @throws the original error for anything that is not a race — a syntax error, a
2148
- * permission failure, a unique constraint the customer's own rows violate.
2149
- */
2150
- async function applyAction(client, action) {
2151
- for (let attempt = 1;; attempt++) try {
2152
- await client.query(action.sql);
2153
- return true;
2154
- } catch (err) {
2155
- if (isDuplicateObjectRace(err)) {
2156
- logger.debug(`[schema] ${action.kind} ${action.target}: already created by another instance`);
2157
- return false;
2158
- }
2159
- if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {
2160
- logger.debug(`[schema] ${action.kind} ${action.target}: lost a race with another instance (attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`);
2161
- await new Promise((resolve) => setTimeout(resolve, 40 * attempt * (1 + Math.random())));
2162
- continue;
2163
- }
2164
- throw err;
2165
- }
2166
- }
2167
- //#endregion
2168
- export { resolveStringColumnLength as _, generatePostgresDdl as a, generatePostgresVectorDdl as c, AUTH_USERS_COLUMNS as d, authUsersColumnSql as f, visibleColumnProjection as g, hiddenColumnsOption as h, readSchemaFactsFor as i, planCollectionPolicies as l, buildSearchColumnSpec as m, planCollectionSchemaEnsure as n, generatePostgresPoliciesDdl as o, SEARCH_UNACCENT_FN as p, readExistingSchema as r, generatePostgresSearchDdl as s, ensure_collection_tables_exports as t, resolveColumnName as u };
1310
+ export { searchIndexStatements as C, searchHelperFunctions as S, assertSearchIsPostgresOnly as _, generatePostgresVectorDdl as a, searchColumnStamps as b, isIdProperty as c, planRelationalColumns as d, quoteSqlLiteral as f, SEARCH_UNACCENT_FN as g, SEARCH_TEXT_FN as h, generatePostgresSearchDdl as i, planCollectionPolicies as l, SEARCH_STAMP_PREFIX as m, generatePostgresDdl as n, generate_postgres_ddl_logic_exports as o, resolveColumnName as p, generatePostgresPoliciesDdl as r, getSqlColumnType as s, defaultBelongsToOnDelete as t, planJunctionTables as u, buildSearchColumnSpec as v, visibleColumnProjection as w, searchExtensionStatements as x, hiddenColumnsOption as y };
2169
1311
 
2170
- //# sourceMappingURL=ensure-collection-tables-DgVixhX3.js.map
1312
+ //# sourceMappingURL=generate-postgres-ddl-logic-CtnpIBVv.js.map