@rebasepro/server-postgres 0.18.0 → 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 (70) hide show
  1. package/dist/BranchService-ByLfM_Tt.js +360 -0
  2. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  3. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  4. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  5. package/dist/arg-Dni7MzLB.js +87 -0
  6. package/dist/arg-Dni7MzLB.js.map +1 -0
  7. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  8. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  9. package/dist/backup-cli-oM9WQVlV.js +408 -0
  10. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  11. package/dist/backup-service-Br3tqwsy.js +715 -0
  12. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  13. package/dist/cli-errors-DxN4yzdH.js +657 -0
  14. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  15. package/dist/cli-output-CNdMql-L.js +47 -0
  16. package/dist/cli-output-CNdMql-L.js.map +1 -0
  17. package/dist/cli.js +2020 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/{collection-index-C7LopMyX.js → collection-index-BHgT_TKd.js} +32 -3
  20. package/dist/{collection-index-C7LopMyX.js.map → collection-index-BHgT_TKd.js.map} +1 -1
  21. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  22. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  23. package/dist/databasePoolManager-Bj5FbeAs.js +349 -0
  24. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  25. package/dist/doctor-D7HWmGzs.js +974 -0
  26. package/dist/doctor-D7HWmGzs.js.map +1 -0
  27. package/dist/{ensure-collection-policies-CagOzEPS.js → ensure-collection-policies-2c2NV8zX.js} +5 -25
  28. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  29. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  30. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  31. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  32. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  33. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  34. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  35. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  36. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  37. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +1312 -0
  38. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  39. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  40. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  41. package/dist/index.es.js +849 -10807
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  44. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  45. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  46. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  47. package/dist/rls-bootstrap-sql-DLDnUjtx.js +7092 -0
  48. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  49. package/dist/{rls-enforcement-ClJoTboT.js → rls-enforcement-DeYEjYLt.js} +14 -3
  50. package/dist/{rls-enforcement-ClJoTboT.js.map → rls-enforcement-DeYEjYLt.js.map} +1 -1
  51. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  52. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  53. package/dist/schema/doctor-cli.js +129 -0
  54. package/dist/schema/doctor-cli.js.map +1 -0
  55. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  56. package/dist/schema/generate-drizzle-schema.js +24 -0
  57. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  58. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  59. package/dist/schema/generate-postgres-ddl.js +138 -0
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  61. package/dist/schema/introspect-db.js +866 -0
  62. package/dist/schema/introspect-db.js.map +1 -0
  63. package/dist/source-Br7L7GOI.js +410 -0
  64. package/dist/source-Br7L7GOI.js.map +1 -0
  65. package/dist/vector-index-DkWxm2de.js +309 -0
  66. package/dist/vector-index-DkWxm2de.js.map +1 -0
  67. package/package.json +7 -7
  68. package/dist/ensure-collection-policies-CagOzEPS.js.map +0 -1
  69. package/dist/ensure-collection-tables-DbAhrjnG.js +0 -2732
  70. package/dist/ensure-collection-tables-DbAhrjnG.js.map +0 -1
@@ -0,0 +1,1312 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
+ import { 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";
10
+ import { createHash } from "node:crypto";
11
+ //#region src/schema/search-column.ts
12
+ /**
13
+ * The one place a collection's `search` block becomes SQL.
14
+ *
15
+ * Four things describe a Postgres table in this codebase — the DDL generator,
16
+ * the Drizzle schema generator, the runtime table builder for BaaS mode, and
17
+ * the boot-time schema ensure — and each of them has, at some point, described
18
+ * a column differently from the others. The `varchar(255)` note in
19
+ * `generate-postgres-ddl-logic` is one such scar: the same property produced a
20
+ * capped column down one path and an uncapped one down the other, and nothing
21
+ * failed until a user hit the cap.
22
+ *
23
+ * So the search column is not implemented four times. It is computed once,
24
+ * here, and every generator renders the same {@link SearchColumnSpec}. There is
25
+ * a test asserting exactly that (`search-column-contract.test.ts`); the point of
26
+ * this module is that the test has something to assert *about*.
27
+ *
28
+ * ## Why the expressions look the way they do
29
+ *
30
+ * A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
31
+ * Postgres is stricter here than intuition. Verified against PostgreSQL 18:
32
+ *
33
+ * | expression | immutable |
34
+ * |-----------------------------------------|-----------|
35
+ * | `to_tsvector('spanish', col)` | yes |
36
+ * | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
37
+ * | `array_to_string(col, ' ')` | **no** |
38
+ * | `col::text` on `text[]` | **no** |
39
+ * | `to_jsonb(col)` | **no** |
40
+ * | `unaccent(col)` | **no** — dictionary lookup is STABLE |
41
+ * | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
42
+ * | `setweight(...) || setweight(...)` | yes |
43
+ *
44
+ * Three of the four things a real search column needs are therefore unavailable
45
+ * directly, which is why {@link searchHelperFunctions} exists: each wraps a
46
+ * stable built-in in an SQL function declared IMMUTABLE. That declaration is a
47
+ * promise, and it is a true one for these three — array joining, JSON string
48
+ * extraction and accent folding are all deterministic for a given input; the
49
+ * built-ins are marked stable only because they must account for element types
50
+ * and dictionaries in general.
51
+ *
52
+ * The alternative was to skip `unaccent` and text arrays entirely. That is not
53
+ * a real option in an accented language: Postgres stems `auditoría` to
54
+ * `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
55
+ * typed without accents misses every row that carries them.
56
+ */
57
+ /** Schema-qualified so a collection outside `public` still resolves them. */
58
+ var HELPER_SCHEMA = "public";
59
+ /**
60
+ * Names of the helper functions. Frozen: they are recorded in the stored
61
+ * generation expression of every search column ever created, so renaming one
62
+ * orphans every table that already has a search column.
63
+ */
64
+ var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
65
+ var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
66
+ /** Raised when a `search` block names something that cannot be searched. */
67
+ var SearchConfigError = class extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "SearchConfigError";
71
+ }
72
+ };
73
+ /** The `search` block of a collection, or undefined when it has none. */
74
+ var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
75
+ /**
76
+ * Refuse a `search` block on a collection this engine does not store.
77
+ *
78
+ * The type only permits one on a `PostgresCollectionConfig`, so TypeScript
79
+ * already stops the ordinary case. This catches the rest — a JS config, a cast,
80
+ * a collection whose `engine` was changed after the block was written — because
81
+ * the alternative is the exact failure the block exists to prevent: a developer
82
+ * who declared what to index, saw no error, and got the substring fallback.
83
+ *
84
+ * Called with *every* collection, before the Postgres ones are filtered out.
85
+ */
86
+ var assertSearchIsPostgresOnly = (collections) => {
87
+ for (const collection of collections) {
88
+ if (isPostgresCollectionConfig(collection)) continue;
89
+ if (!collection.search) continue;
90
+ const engine = collection.engine ?? "non-postgres";
91
+ throw new SearchConfigError(`${collection.slug}.search: full-text search is a Postgres feature, and this collection is served by \`${engine}\`. Remove the block — it would otherwise look configured while \`.search()\` kept using the default substring match.`);
92
+ }
93
+ };
94
+ var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
95
+ /**
96
+ * Classify a property for search purposes.
97
+ *
98
+ * Deliberately narrower than `getSqlColumnType`: search only cares whether a
99
+ * value reaches text, and the mapping from property to *physical* type is
100
+ * asserted against `getSqlColumnType` in the contract test rather than
101
+ * duplicated here.
102
+ *
103
+ * Returns null for anything that is not text-bearing, which the caller turns
104
+ * into a boot error naming the property.
105
+ */
106
+ var classify = (prop) => {
107
+ switch (prop.type) {
108
+ case "string": {
109
+ const sp = prop;
110
+ if (sp.enum) return {
111
+ kind: "text",
112
+ reason: "enum"
113
+ };
114
+ if (sp.isId === "uuid" || sp.columnType === "uuid") return {
115
+ kind: "text",
116
+ reason: "uuid"
117
+ };
118
+ return { kind: "text" };
119
+ }
120
+ case "map":
121
+ if (prop.columnType === "json") return {
122
+ kind: "jsonb",
123
+ reason: "json"
124
+ };
125
+ return { kind: "jsonb" };
126
+ case "array": {
127
+ const ap = prop;
128
+ let colType = ap.columnType;
129
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
130
+ const of = ap.of;
131
+ if (of.type === "string") colType = "text[]";
132
+ else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
133
+ else if (of.type === "boolean") colType = "boolean[]";
134
+ }
135
+ if (colType === "text[]") return { kind: "text_array" };
136
+ if (colType === "json") return {
137
+ kind: "jsonb",
138
+ reason: "json"
139
+ };
140
+ if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
141
+ kind: "text_array",
142
+ reason: "non_text_array"
143
+ };
144
+ return { kind: "jsonb" };
145
+ }
146
+ default: return null;
147
+ }
148
+ };
149
+ var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
150
+ /** SQL reading one field as plain text, before normalization. */
151
+ var rawTextSql = (field) => {
152
+ const col = `"${field.column}"`;
153
+ if (field.kind === "text") return `coalesce(${col}, '')`;
154
+ if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
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))`;
156
+ };
157
+ var quote = (v) => `'${v.replace(/'/g, "''")}'`;
158
+ /**
159
+ * Resolve and validate one declared field path.
160
+ *
161
+ * A path that does not resolve throws. The whole point of an explicit block is
162
+ * that the author knows what is indexed; a silently dropped field would make it
163
+ * a guess again, and the failure — a search that returns nothing for content
164
+ * that is plainly in the row — is invisible from the outside.
165
+ */
166
+ var resolveField = (entry, collection, cfg) => {
167
+ const path = typeof entry === "string" ? entry : entry.path;
168
+ const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? DEFAULT_SEARCH_WEIGHT;
169
+ const where = `${collection.slug}.search`;
170
+ if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
171
+ const [head, ...rest] = path.split(".");
172
+ const prop = collection.properties?.[head];
173
+ if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
174
+ const classified = classify(prop);
175
+ if (!classified) throw new SearchConfigError(`${where}: "${path}" is a \`${prop.type}\` property, which holds no text to search. Searchable kinds are \`string\`, \`string[]\` and \`map\` (or a path inside one).`);
176
+ if (classified.reason === "enum") throw new SearchConfigError(`${where}: "${path}" is an enum. Enums are a fixed vocabulary — filter on them with \`where\` instead, which is exact and uses an index.`);
177
+ if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
178
+ if (classified.reason === "json") throw new SearchConfigError(`${where}: "${path}" is a \`json\` column, and the cast from \`json\` to \`jsonb\` is not immutable, so it cannot feed a generated column. Declare the property as \`jsonb\` (the default) to search it.`);
179
+ if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
180
+ if (rest.length > 0 && classified.kind !== "jsonb") throw new SearchConfigError(`${where}: "${path}" addresses a path inside "${head}", but "${head}" is a \`${prop.type}\` property, not a \`map\`. Only map properties have paths inside them.`);
181
+ const column = columnNameOf(head, prop);
182
+ const textSql = normalize(rawTextSql({
183
+ column,
184
+ jsonPath: rest,
185
+ kind: classified.kind
186
+ }), cfg.unaccent === true);
187
+ const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;
188
+ return {
189
+ path,
190
+ column,
191
+ jsonPath: rest,
192
+ kind: classified.kind,
193
+ weight,
194
+ sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
195
+ textSql
196
+ };
197
+ };
198
+ /**
199
+ * Build the full spec for a collection, or undefined when it has not opted in.
200
+ *
201
+ * Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
202
+ * at boot surface that as a startup failure — a search block that half-works is
203
+ * worse than one that refuses.
204
+ */
205
+ var buildSearchColumnSpec = (collection) => {
206
+ const cfg = getSearchConfig(collection);
207
+ if (!cfg) return void 0;
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.`);
209
+ const table = getTableName(collection);
210
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
211
+ const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;
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.`);
213
+ const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
214
+ const seen = /* @__PURE__ */ new Set();
215
+ for (const f of fields) {
216
+ if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
217
+ seen.add(f.path);
218
+ }
219
+ const extensions = [];
220
+ if (cfg.unaccent) extensions.push("unaccent");
221
+ if (cfg.fuzzy) extensions.push("pg_trgm");
222
+ const spec = {
223
+ schema,
224
+ table,
225
+ column,
226
+ language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,
227
+ unaccent: cfg.unaccent === true,
228
+ fields,
229
+ expression: fields.map((f) => f.sql).join(" || "),
230
+ indexName: toPostgresIdentifier(`${table}_${column}_gin`),
231
+ extensions
232
+ };
233
+ if (cfg.fuzzy) {
234
+ const fuzzyColumn = `${column}_text`;
235
+ if (collection.properties?.[fuzzyColumn]) throw new SearchConfigError(`${collection.slug}.search: \`fuzzy\` needs the column "${fuzzyColumn}", which collides with a declared property. Set \`search.column\` to something else.`);
236
+ spec.fuzzy = {
237
+ column: fuzzyColumn,
238
+ expression: fields.map((f) => f.textSql).join(" || ' ' || "),
239
+ indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
240
+ threshold: cfg.fuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD
241
+ };
242
+ }
243
+ return spec;
244
+ };
245
+ /**
246
+ * The IMMUTABLE wrappers the generated expressions call.
247
+ *
248
+ * `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
249
+ * than an error, and idempotent for the same reason every other boot-time DDL
250
+ * statement here is.
251
+ *
252
+ * The bodies are stable built-ins wrapped in an immutable promise — see the
253
+ * module comment for why that promise is sound. `STRICT` matters: it makes NULL
254
+ * in mean NULL out without executing the body, which is what the `coalesce` at
255
+ * each call site then absorbs.
256
+ */
257
+ var searchHelperFunctions = (spec) => {
258
+ const statements = [`CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(text[]) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT array_to_string($1, ' ') $$;`, `CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(jsonb) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT coalesce(string_agg(v, ' '), '')\n FROM jsonb_array_elements_text(jsonb_path_query_array($1, 'strict $.**?(@.type() == "string")')) AS v $$;`];
259
+ if (spec.unaccent) statements.push(`CREATE OR REPLACE FUNCTION ${SEARCH_UNACCENT_FN}(text) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT ${HELPER_SCHEMA}.unaccent('${HELPER_SCHEMA}.unaccent'::regdictionary, $1) $$;`);
260
+ return statements;
261
+ };
262
+ /**
263
+ * `CREATE EXTENSION` statements the spec's expressions depend on.
264
+ *
265
+ * `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
266
+ * `CREATE EXTENSION` installs into the first schema on `search_path`, which
267
+ * defaults to `"$user", public` — and the scaffold's database role is named
268
+ * `rebase`, the same as the schema the generator creates one statement earlier.
269
+ * So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
270
+ * dictionary in `rebase`, and every reference to `public.unaccent` below fails
271
+ * with "text search dictionary does not exist". Observed, not theorised.
272
+ */
273
+ var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
274
+ /** The column definition as it appears inside `CREATE TABLE`. */
275
+ var searchColumnDefinition = (spec) => `"${spec.column}" tsvector GENERATED ALWAYS AS (${spec.expression}) STORED`;
276
+ /** The fuzzy column definition, when the spec asks for one. */
277
+ var fuzzyColumnDefinition = (spec) => spec.fuzzy ? `"${spec.fuzzy.column}" text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED` : void 0;
278
+ /**
279
+ * Index statements for the spec.
280
+ *
281
+ * `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
282
+ * SQL file replayed as one unit — a migration, or `search.sql` — where a
283
+ * concurrent build is not allowed. The boot-time ensure path runs statement by
284
+ * statement against tables that are live and populated, and uses the
285
+ * concurrent form instead; see `ensureSearchColumns`.
286
+ */
287
+ var searchIndexStatements = (spec) => {
288
+ const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
289
+ if (spec.fuzzy) statements.push(`CREATE INDEX IF NOT EXISTS "${spec.fuzzy.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.fuzzy.column}" ${HELPER_SCHEMA}.gin_trgm_ops);`);
290
+ return statements;
291
+ };
292
+ /**
293
+ * Marker on the comment of every generated search column this module creates.
294
+ *
295
+ * Versioned because the fingerprint below is only comparable against itself: a
296
+ * future change to how it is computed has to read as "not stamped by this
297
+ * version" rather than as drift on every existing column.
298
+ */
299
+ var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
300
+ /**
301
+ * A stable fingerprint of one generated column's expression.
302
+ *
303
+ * Why a stamp rather than reading the expression back: Postgres stores a
304
+ * generated column's expression *parsed*, and hands it back deparsed — casts
305
+ * made explicit, identifiers requoted, schema qualifications added or dropped
306
+ * according to `search_path`. Comparing that text to the text we generated
307
+ * would report drift on wording, and this comparison decides whether a boot
308
+ * refuses, so a false positive is an outage. The stamp is written by the same
309
+ * code that writes the column, so equality means what it says.
310
+ */
311
+ var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
312
+ /**
313
+ * The stamps for a spec's generated columns — one per column, never shared.
314
+ *
315
+ * Per column on purpose: turning `fuzzy` on adds a second column and changes
316
+ * nothing about the first, and a spec-wide fingerprint would report the
317
+ * untouched `tsvector` column as drifted and refuse a boot over a change that
318
+ * is purely additive.
319
+ */
320
+ var searchColumnStamps = (spec) => {
321
+ const stamp = (column, expression) => {
322
+ const fingerprint = searchExpressionFingerprint(expression);
323
+ return {
324
+ column,
325
+ expression,
326
+ fingerprint,
327
+ sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
328
+ };
329
+ };
330
+ const stamps = [stamp(spec.column, spec.expression)];
331
+ if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
332
+ return stamps;
333
+ };
334
+ /**
335
+ * The same drift check as the boot ensure, for the SQL file.
336
+ *
337
+ * Needed because {@link searchColumnStamps} would otherwise *launder* drift on
338
+ * the migration path: `ADD COLUMN IF NOT EXISTS` does nothing to a column that
339
+ * exists, so a re-generated `search.sql` would stamp a stale column with the
340
+ * new block's fingerprint and the next boot would find them in agreement.
341
+ * Guarding first means the file refuses instead — `rebase db push` is attended,
342
+ * and the operator reading the failure is the person who changed the block.
343
+ */
344
+ var searchStampGuards = (spec) => searchColumnStamps(spec).map((stamp) => {
345
+ const relation = quote(`"${spec.schema}"."${spec.table}"`);
346
+ return `DO $rebase_search$
347
+ DECLARE recorded text;
348
+ BEGIN
349
+ SELECT col_description(a.attrelid, a.attnum) INTO recorded
350
+ FROM pg_attribute a
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
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;
354
+ END IF;
355
+ END
356
+ $rebase_search$;`;
357
+ });
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
+ /**
366
+ * The generated column names a collection's search block adds, if any.
367
+ *
368
+ * These are physical columns on the table, so `SELECT *` returns them. They are
369
+ * an index in column form — a list of lexeme positions, or a concatenation of
370
+ * every searchable field on the row — and nothing outside the query planner has
371
+ * any use for them. Left in, every list response carries a second, larger copy
372
+ * of the row's text.
373
+ */
374
+ var searchColumnNames = (collection) => {
375
+ let spec;
376
+ try {
377
+ spec = buildSearchColumnSpec(collection);
378
+ } catch {
379
+ return [];
380
+ }
381
+ if (!spec) return [];
382
+ return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
383
+ };
384
+ /**
385
+ * True for a column whose type only ever holds a search index.
386
+ *
387
+ * Independent of any collection config on purpose: an introspected database
388
+ * (BaaS mode) can carry a `tsvector` column this framework never created —
389
+ * Pagila's `film.fulltext` is the canonical one — and it should not be returned
390
+ * to callers either. `isDerivedIndexColumn` already keeps such a column out of
391
+ * the *properties*; this keeps it out of the *rows*.
392
+ */
393
+ var isSearchIndexColumn = (column) => {
394
+ const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
395
+ return sqlType === "tsvector" || sqlType === "tsquery";
396
+ };
397
+ /**
398
+ * A drizzle select projection over `table` with the search columns dropped.
399
+ *
400
+ * Returns undefined when nothing needs dropping, so the common case keeps using
401
+ * a plain `select()` and this stays invisible in the generated SQL.
402
+ */
403
+ var visibleColumnProjection = (tableColumns, collection) => {
404
+ const excluded = excludedColumnNames(tableColumns, collection);
405
+ if (!tableColumns || excluded.length === 0) return void 0;
406
+ const projection = {};
407
+ for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
408
+ return projection;
409
+ };
410
+ /** The same exclusion as a drizzle `db.query` `columns` denylist. */
411
+ var hiddenColumnsOption = (tableColumns, collection) => {
412
+ const excluded = excludedColumnNames(tableColumns, collection);
413
+ if (excluded.length === 0) return void 0;
414
+ return Object.fromEntries(excluded.map((name) => [name, false]));
415
+ };
416
+ /**
417
+ * The columns to keep out of a response, by name.
418
+ *
419
+ * `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
420
+ * for anything that is not a real drizzle table — a stub in a test, a derived
421
+ * or nested path with no table behind it. Nothing to exclude is the right
422
+ * answer there, and it has to be an answer rather than a throw: this runs on
423
+ * the read path of every collection, opted in or not.
424
+ */
425
+ var excludedColumnNames = (tableColumns, collection) => {
426
+ if (!tableColumns || typeof tableColumns !== "object") return [];
427
+ const byName = new Set(collection ? searchColumnNames(collection) : []);
428
+ return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
429
+ };
430
+ //#endregion
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
+ });
452
+ var resolveColumnName = (propName, prop) => {
453
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
454
+ return toSnakeCase(propName);
455
+ };
456
+ var getPrimaryKeyProp = (collection) => {
457
+ if (collection.properties) {
458
+ const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
459
+ if (idPropEntry) {
460
+ const prop = idPropEntry[1];
461
+ const isUuid = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
462
+ return {
463
+ name: idPropEntry[0],
464
+ type: prop.type === "number" ? "number" : "string",
465
+ isUuid
466
+ };
467
+ }
468
+ }
469
+ const idProp = collection.properties?.["id"];
470
+ if (idProp?.type === "number") return {
471
+ name: "id",
472
+ type: "number",
473
+ isUuid: false
474
+ };
475
+ return {
476
+ name: "id",
477
+ type: "string",
478
+ isUuid: idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid"
479
+ };
480
+ };
481
+ var isNumericId = (collection) => {
482
+ return getPrimaryKeyProp(collection).type === "number";
483
+ };
484
+ var getPrimaryKeyName = (collection) => {
485
+ return getPrimaryKeyProp(collection).name;
486
+ };
487
+ /** The column type a junction holds for one endpoint's primary key. */
488
+ var junctionKeyType = (collection) => isNumericId(collection) ? "INTEGER" : getPrimaryKeyProp(collection).isUuid ? "UUID" : "TEXT";
489
+ var isIdProperty = (propName, prop, collection) => {
490
+ if ("isId" in prop && Boolean(prop.isId)) return true;
491
+ return !Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId)) && propName === "id";
492
+ };
493
+ /**
494
+ * Render statements produced by {@link generatePolicyStatements} back into the
495
+ * exact string the DDL/policies files have always carried: each statement on
496
+ * its own line, terminated by a newline. Keeping the string form derived from
497
+ * the statement array means the two can never drift — the boot-time applier and
498
+ * the generated `policies.sql` emit the same SQL, from the same source.
499
+ */
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";
529
+ var generatePolicyDdl = (collection, rule, resolveCollection) => statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));
530
+ /**
531
+ * The individual SQL statements a single security rule compiles to: a
532
+ * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
533
+ * statement (terminated by `;`, no trailing newline).
534
+ *
535
+ * This is the primitive the boot-time RLS applier runs one statement at a time
536
+ * (the runtime's DB handle speaks the extended query protocol, which forbids
537
+ * multiple commands in one execute), while `db push` writes the joined string.
538
+ */
539
+ var generatePolicyStatements = (collection, rule, resolveCollection) => {
540
+ const tableName = getTableName(collection);
541
+ const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
542
+ const policyNames = getPolicyNamesForRule(rule, tableName);
543
+ return ops.flatMap((op, opIdx) => {
544
+ return generateSinglePolicyStatements(collection, rule, op, policyNames[opIdx], resolveCollection);
545
+ });
546
+ };
547
+ var generateSinglePolicyStatements = (collection, rule, operation, policyName, resolveCollection) => {
548
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
549
+ const tableName = getTableName(collection);
550
+ const mode = (rule.mode ?? "permissive").toUpperCase();
551
+ const operationUpper = operation.toUpperCase();
552
+ const pgRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
553
+ const needsUsing = operation !== "insert";
554
+ const needsWithCheck = operation !== "select" && operation !== "delete";
555
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
556
+ let usingClause = needsUsing && usingExpr ? policyToPostgres(usingExpr, collection, { resolveCollection }) : null;
557
+ let withCheckClause = needsWithCheck && withCheckExpr ? policyToPostgres(withCheckExpr, collection, { resolveCollection }) : null;
558
+ if (!usingClause && needsUsing) usingClause = "false";
559
+ if (!withCheckClause && needsWithCheck) withCheckClause = "false";
560
+ const drop = `DROP POLICY IF EXISTS "${policyName}" ON "${schema}"."${tableName}";`;
561
+ let create = `CREATE POLICY "${policyName}" ON "${schema}"."${tableName}" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map((r) => `"${r}"`).join(", ")}`;
562
+ if (usingClause) create += ` USING (${usingClause})`;
563
+ if (withCheckClause) create += ` WITH CHECK (${withCheckClause})`;
564
+ create += ";";
565
+ return [drop, create];
566
+ };
567
+ /**
568
+ * Single-quote escaping for a SQL string literal (PostgreSQL doubles the
569
+ * quote). Enum labels come straight from user-authored collection config, so a
570
+ * label like `it's` closes the literal early and the whole generated file stops
571
+ * parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
572
+ * because ensure-collection-tables already imports from this module — the
573
+ * reverse would be a cycle.
574
+ */
575
+ var quoteSqlLiteral = (value) => `'${value.replace(/'/g, "''")}'`;
576
+ var getSqlColumnType = (propName, prop, collection, collections) => {
577
+ switch (prop.type) {
578
+ case "string": {
579
+ const stringProp = prop;
580
+ if (stringProp.enum) {
581
+ const tableName = getTableName(collection);
582
+ const colName = resolveColumnName(propName, prop);
583
+ return `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${tableName}_${colName}"`;
584
+ }
585
+ if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") return "UUID";
586
+ if (stringProp.columnType === "char") return `CHAR(${resolveStringColumnLength(stringProp)})`;
587
+ if (stringProp.columnType === "varchar") return `VARCHAR(${resolveStringColumnLength(stringProp)})`;
588
+ return "TEXT";
589
+ }
590
+ case "number": {
591
+ const numProp = prop;
592
+ const isId = isIdProperty(propName, prop, collection);
593
+ if ("isId" in numProp && numProp.isId === "increment") return "INTEGER GENERATED BY DEFAULT AS IDENTITY";
594
+ if (numProp.columnType) {
595
+ if (numProp.columnType === "double precision") return "DOUBLE PRECISION";
596
+ return numProp.columnType.toUpperCase();
597
+ }
598
+ return numProp.validation?.integer || isId ? "INTEGER" : "NUMERIC";
599
+ }
600
+ case "boolean": return "BOOLEAN";
601
+ case "date": {
602
+ const dateProp = prop;
603
+ if (dateProp.columnType === "date") return "DATE";
604
+ if (dateProp.columnType === "time") return "TIME";
605
+ return "TIMESTAMP WITH TIME ZONE";
606
+ }
607
+ case "map": return prop.columnType === "json" ? "JSON" : "JSONB";
608
+ case "geopoint": return "JSONB";
609
+ case "array": {
610
+ const arrayProp = prop;
611
+ let colType = arrayProp.columnType;
612
+ if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
613
+ const ofProp = arrayProp.of;
614
+ if (ofProp.type === "string") colType = "text[]";
615
+ else if (ofProp.type === "number") colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
616
+ else if (ofProp.type === "boolean") colType = "boolean[]";
617
+ }
618
+ if (colType === "json") return "JSON";
619
+ if (colType === "text[]") return "TEXT[]";
620
+ if (colType === "integer[]") return "INTEGER[]";
621
+ if (colType === "boolean[]") return "BOOLEAN[]";
622
+ if (colType === "numeric[]") return "NUMERIC[]";
623
+ return "JSONB";
624
+ }
625
+ case "vector": return `VECTOR(${prop.dimensions})`;
626
+ case "binary": return "BYTEA";
627
+ case "relation": {
628
+ const refProp = prop;
629
+ const relation = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
630
+ if (relation?.kind !== "belongsTo") throw new Error(`Relation ${propName} does not put a column on this table (only \`belongsTo\` does)`);
631
+ let targetCollection;
632
+ try {
633
+ targetCollection = relation.target();
634
+ } catch {
635
+ return "TEXT";
636
+ }
637
+ const pkProp = getPrimaryKeyProp(targetCollection);
638
+ return pkProp.type === "number" ? "INTEGER" : pkProp.isUuid ? "UUID" : "TEXT";
639
+ }
640
+ case "reference": {
641
+ const refProp = prop;
642
+ const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
643
+ if (!targetCollection) return "TEXT";
644
+ const pkProp = getPrimaryKeyProp(targetCollection);
645
+ return pkProp.type === "number" ? "INTEGER" : pkProp.isUuid ? "UUID" : "TEXT";
646
+ }
647
+ default: throw new Error(`No Postgres column type for property '${propName}' of type '${prop.type}' in collection '${collection.slug}'. Add a case to \`getSqlColumnType\` (and to \`getDrizzleColumn\`, which must agree).`);
648
+ }
649
+ };
650
+ /**
651
+ * Everything a `search` block needs, as a file Rebase applies itself.
652
+ *
653
+ * Search is the one part of the schema Atlas does not own. Two independent
654
+ * reasons, and either alone would be enough:
655
+ *
656
+ * 1. Its free tier refuses to *parse* a desired-state file that so much as
657
+ * contains a function — "functions and procedures are available to
658
+ * logged-in users only". A generated `tsvector` column cannot avoid one:
659
+ * `unaccent` is STABLE and jsonb flattening needs a set-returning function,
660
+ * so both have to be wrapped in an IMMUTABLE helper to be legal in a
661
+ * generated column at all.
662
+ * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs
663
+ * against, so a helper seeded there beforehand is gone by the time the plan
664
+ * is analysed. There is no hook to reinstate it.
665
+ *
666
+ * So the column, its index and its helpers are excluded from Atlas's view
667
+ * (`searchExcludePatterns`) and applied from here — the same arrangement the
668
+ * RLS policies already use, and for the same underlying reason.
669
+ *
670
+ * Ordered as it must run: extensions, then helpers, then the column whose
671
+ * expression calls them, then the index over that column. Every statement is
672
+ * `IF NOT EXISTS` / `OR REPLACE`, because this is replayed on every push and
673
+ * appended to migrations that run against databases at any stage of their
674
+ * life. Empty when nothing opted in — the caller writes no file then.
675
+ *
676
+ * The dev database Atlas plans against needs none of this. `searchExcludePatterns`
677
+ * removes the search column from the *inspected* state, so the state Atlas
678
+ * replays there never mentions the column and never calls a helper. Seeding the
679
+ * helpers into that database was tried and removed: measured on Atlas 1.2.3, a
680
+ * `schema apply --dry-run` produces byte-identical output seeded or not, and
681
+ * Atlas wipes the dev database before it plans anyway — so a seed could not have
682
+ * helped even where the excludes are missing, which is the case that really does
683
+ * fail with `function public.rebase_search_unaccent(text) does not exist`.
684
+ */
685
+ var generatePostgresSearchDdl = (allCollections) => {
686
+ const collections = relationalCollections(allCollections);
687
+ const specs = collections.map((c) => buildSearchColumnSpec(c)).filter((s) => s !== void 0);
688
+ if (specs.length === 0) return "";
689
+ const extensions = Array.from(new Set(specs.flatMap(searchExtensionStatements)));
690
+ const helpers = Array.from(new Set(specs.flatMap(searchHelperFunctions)));
691
+ let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n";
692
+ ddl += "--\n";
693
+ ddl += "-- Full-text search for the collections declaring a `search` block.\n";
694
+ ddl += "-- Applied by Rebase, not by Atlas — see generatePostgresSearchDdl.\n\n";
695
+ extensions.forEach((s) => {
696
+ ddl += `${s}\n`;
697
+ });
698
+ if (extensions.length > 0) ddl += "\n";
699
+ helpers.forEach((s) => {
700
+ ddl += `${s}\n\n`;
701
+ });
702
+ for (const collection of collections) {
703
+ const spec = buildSearchColumnSpec(collection);
704
+ if (!spec) continue;
705
+ const table = `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${getTableName(collection)}"`;
706
+ searchStampGuards(spec).forEach((s) => {
707
+ ddl += `${s}\n`;
708
+ });
709
+ ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${searchColumnDefinition(spec)};\n`;
710
+ const fuzzyDef = fuzzyColumnDefinition(spec);
711
+ if (fuzzyDef) ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${fuzzyDef};\n`;
712
+ searchColumnStamps(spec).forEach((s) => {
713
+ ddl += `${s.sql}\n`;
714
+ });
715
+ searchIndexStatements(spec).forEach((s) => {
716
+ ddl += `${s}\n`;
717
+ });
718
+ ddl += "\n";
719
+ }
720
+ return ddl;
721
+ };
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
+ /**
748
+ * Everything a `{ type: "vector" }` property needs, as a file Rebase applies
749
+ * itself.
750
+ *
751
+ * Vector is the second part of the schema Atlas does not own, and it is out for
752
+ * a narrower reason than search: not that Atlas refuses to parse the file, but
753
+ * that it cannot *execute* it. To compute a desired state Atlas materialises
754
+ * `schema.sql` in a dev database, that database is created empty, and Atlas
755
+ * empties it again at the start of every run — so `VECTOR(384)` resolves
756
+ * against a database that structurally cannot have pgvector, and every push
757
+ * dies with `type "vector" does not exist`. Seeding the extension beforehand
758
+ * does not survive the clean (measured: present before the run, gone after),
759
+ * putting `CREATE EXTENSION` in `schema.sql` is refused as an Atlas Pro
760
+ * feature, and `--exclude` alone cannot help because Atlas still parses and
761
+ * applies the whole file.
762
+ *
763
+ * So the column, its ANN indexes and the extension are excluded from Atlas's
764
+ * view ({@link vectorExcludePatterns}) and applied from here — the same
765
+ * arrangement search and the RLS policies already use.
766
+ *
767
+ * `CREATE EXTENSION` appears only when the project's database named `vector` in
768
+ * its `extensions` — see `DatabaseOptions.extensions`. Everything else is
769
+ * emitted either way: a database that already has pgvector installed by hand
770
+ * needs no permission from anyone, and one that does not gets the column
771
+ * refused by Postgres with a message this repo makes name the opt-in.
772
+ *
773
+ * Ordered as it must run: the extension, then the drift guard, then the column
774
+ * whose type needs the extension, then the indexes over that column. Every
775
+ * statement is `IF NOT EXISTS`, because this is replayed on every push and
776
+ * appended to migrations that run against databases at any stage of their life.
777
+ * Empty when no collection declares a vector — the caller writes no file then.
778
+ */
779
+ var generatePostgresVectorDdl = (allCollections, options = {}) => {
780
+ const withVectors = relationalCollections(allCollections).filter((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0);
781
+ if (withVectors.length === 0) return "";
782
+ let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n";
783
+ ddl += "--\n";
784
+ ddl += "-- Vector columns and their ANN indexes, for the collections declaring a\n";
785
+ ddl += "-- `vector` property. Applied by Rebase, not by Atlas — see\n";
786
+ ddl += "-- generatePostgresVectorDdl.\n\n";
787
+ ddl += vectorExtensionDeclared(options.extensions) ? `${vectorExtensionStatement()}\n\n` : `-- pgvector is not installed from here: no database declared it. To let Rebase\n-- install it, add ${VECTOR_EXTENSION_OPT_IN} in config/resources.ts.\n-- Otherwise install it once by hand; the column below needs the type either way.\n\n`;
788
+ for (const collection of withVectors) {
789
+ const table = `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${getTableName(collection)}"`;
790
+ for (const spec of buildVectorColumnSpecs(collection, resolveColumnName)) {
791
+ ddl += `${vectorDimensionGuard(spec)}\n`;
792
+ ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${vectorColumnDefinition(spec)};\n`;
793
+ }
794
+ const plan = buildVectorIndexPlan(collection, resolveColumnName);
795
+ vectorIndexStatements(plan).forEach((s) => {
796
+ ddl += `${s}\n`;
797
+ });
798
+ for (const skip of plan.skipped) ddl += `-- No ANN index on "${skip.schema}"."${skip.table}"."${skip.column}": ${skip.reason}\n`;
799
+ ddl += "\n";
800
+ }
801
+ return ddl;
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
+ };
824
+ var generatePostgresDdl = async (allCollections, options = {
825
+ includePolicies: true,
826
+ includeSearch: true,
827
+ includeVector: true
828
+ }) => {
829
+ const collections = relationalCollections(allCollections);
830
+ let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n\n";
831
+ const uniqueSchemas = Array.from(/* @__PURE__ */ new Set([REBASE_SCHEMA, ...collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)]));
832
+ uniqueSchemas.forEach((schema) => {
833
+ if (schema) ddl += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
834
+ });
835
+ if (uniqueSchemas.length > 0) ddl += "\n";
836
+ const searchSpecs = collections.map((c) => buildSearchColumnSpec(c)).filter((s) => s !== void 0);
837
+ if (searchSpecs.length > 0) if (options.includeSearch === false) ddl += "-- Full-text search support lives in `search.sql`, applied separately.\n\n";
838
+ else {
839
+ const extensions = Array.from(new Set(searchSpecs.flatMap(searchExtensionStatements)));
840
+ const helpers = Array.from(new Set(searchSpecs.flatMap(searchHelperFunctions)));
841
+ ddl += "-- Full-text search support (collections declaring a `search` block)\n";
842
+ extensions.forEach((s) => {
843
+ ddl += `${s}\n`;
844
+ });
845
+ if (extensions.length > 0) ddl += "\n";
846
+ helpers.forEach((s) => {
847
+ ddl += `${s}\n\n`;
848
+ });
849
+ }
850
+ if (options.includeVector === false && collections.some((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0)) ddl += "-- Vector columns and their ANN indexes live in `vector.sql`, applied separately.\n\n";
851
+ const emittedEnums = /* @__PURE__ */ new Set();
852
+ collections.forEach((collection) => {
853
+ const collectionTable = getTableName(collection);
854
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
855
+ Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
856
+ if ("enum" in prop && (prop.type === "string" || prop.type === "number") && prop.enum) {
857
+ const enumDbName = `${collectionTable}_${resolveColumnName(propName, prop)}`;
858
+ const values = Array.isArray(prop.enum) ? prop.enum.map((v) => String(typeof v === "object" && v !== null && "id" in v ? v.id : v)) : Object.keys(prop.enum);
859
+ if (values.length > 0 && !emittedEnums.has(`${schema}.${enumDbName}`)) {
860
+ emittedEnums.add(`${schema}.${enumDbName}`);
861
+ ddl += `CREATE TYPE "${schema}"."${enumDbName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});\n`;
862
+ }
863
+ }
864
+ });
865
+ });
866
+ if (ddl.endsWith(";\n")) ddl += "\n";
867
+ const junctionSpecs = resolveJunctionSpecs(collections);
868
+ const allTablesToGenerate = /* @__PURE__ */ new Map();
869
+ for (const collection of collections) {
870
+ const tableName = getTableName(collection);
871
+ if (tableName) allTablesToGenerate.set(tableName, { collection });
872
+ const resolvedRelations = resolveCollectionRelations(collection);
873
+ for (const relation of Object.values(resolvedRelations)) if (isManyToMany(relation)) {
874
+ const junctionTableName = relation.through.table;
875
+ if (!allTablesToGenerate.has(junctionTableName)) allTablesToGenerate.set(junctionTableName, {
876
+ collection: {
877
+ table: junctionTableName,
878
+ properties: {}
879
+ },
880
+ isJunction: true,
881
+ relation,
882
+ sourceCollection: collection
883
+ });
884
+ }
885
+ }
886
+ const fkStatements = [];
887
+ const indexStatements = [];
888
+ const policyStatements = [];
889
+ for (const [tableName, { collection, isJunction, relation, sourceCollection }] of allTablesToGenerate.entries()) {
890
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
891
+ const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
892
+ if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
893
+ const targetCollection = relation.target();
894
+ const sourceTable = getTableName(sourceCollection);
895
+ const targetTable = getTableName(targetCollection);
896
+ const sourceSchema = isPostgresCollectionConfig(sourceCollection) && sourceCollection.schema ? sourceCollection.schema : "public";
897
+ const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
898
+ const { sourceColumn, targetColumn } = relation.through;
899
+ const sourceColType = isNumericId(sourceCollection) ? "INTEGER" : getPrimaryKeyProp(sourceCollection).isUuid ? "UUID" : "TEXT";
900
+ const targetColType = isNumericId(targetCollection) ? "INTEGER" : getPrimaryKeyProp(targetCollection).isUuid ? "UUID" : "TEXT";
901
+ const sourceId = getPrimaryKeyName(sourceCollection);
902
+ const targetId = getPrimaryKeyName(targetCollection);
903
+ const onDelete = relation.onDelete ?? "CASCADE";
904
+ ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
905
+ ddl += ` "${sourceColumn}" ${sourceColType} NOT NULL,\n`;
906
+ ddl += ` "${targetColumn}" ${targetColType} NOT NULL,\n`;
907
+ ddl += ` PRIMARY KEY ("${sourceColumn}", "${targetColumn}")\n`;
908
+ ddl += `);\n\n`;
909
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${sourceColumn}_fkey`)}" FOREIGN KEY ("${sourceColumn}") REFERENCES "${sourceSchema}"."${sourceTable}" ("${sourceId}") ON DELETE ${onDelete.toUpperCase()};`);
910
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${targetColumn}_fkey`)}" FOREIGN KEY ("${targetColumn}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
911
+ if (options.includePolicies) {
912
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
913
+ ddl += `\n`;
914
+ const spec = junctionSpecs.get(baseTableName);
915
+ if (spec) {
916
+ const junctionCollection = getJunctionCollectionConfig(spec);
917
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
918
+ getJunctionSecurityRules(spec).forEach((rule) => {
919
+ policyStatements.push(generatePolicyDdl(junctionCollection, rule, resolveCollection));
920
+ });
921
+ }
922
+ }
923
+ } else if (!isJunction) {
924
+ ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
925
+ const columns = [];
926
+ Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
927
+ if (prop.type === "vector" && options.includeVector === false) return;
928
+ if (prop.type === "relation") {
929
+ const refProp = prop;
930
+ const relInfo = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
931
+ if (relInfo?.kind !== "belongsTo") return;
932
+ if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) return;
933
+ let targetCollection;
934
+ try {
935
+ targetCollection = relInfo.target();
936
+ } catch {
937
+ return;
938
+ }
939
+ const targetTable = getTableName(targetCollection);
940
+ const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
941
+ const targetId = getPrimaryKeyName(targetCollection);
942
+ const fkColType = getSqlColumnType(propName, prop, collection, collections);
943
+ const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : "";
944
+ const required = prop.validation?.required;
945
+ const onDeleteVal = relInfo.onDelete ?? defaultBelongsToOnDelete(required);
946
+ let colDef = ` "${relInfo.localKey}" ${fkColType}`;
947
+ if (required) colDef += " NOT NULL";
948
+ columns.push(colDef);
949
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${relInfo.localKey}_fkey`)}" FOREIGN KEY ("${relInfo.localKey}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDeleteVal.toUpperCase()}${onUpdate};`);
950
+ } else if (prop.type === "reference") {
951
+ const refProp = prop;
952
+ const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
953
+ const colName = resolveColumnName(propName, prop);
954
+ const colType = getSqlColumnType(propName, prop, collection, collections);
955
+ const required = prop.validation?.required;
956
+ if (!targetCollection) {
957
+ let colDef = ` "${colName}" ${colType}`;
958
+ if (required) colDef += " NOT NULL";
959
+ columns.push(colDef);
960
+ } else {
961
+ const targetTable = getTableName(targetCollection);
962
+ const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
963
+ const targetId = getPrimaryKeyName(targetCollection);
964
+ const onDelete = defaultBelongsToOnDelete(required);
965
+ let colDef = ` "${colName}" ${colType}`;
966
+ if (required) colDef += " NOT NULL";
967
+ columns.push(colDef);
968
+ fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${colName}_fkey`)}" FOREIGN KEY ("${colName}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
969
+ }
970
+ } else {
971
+ const colName = resolveColumnName(propName, prop);
972
+ const authDefinition = isAuthCollection(collection) ? authUsersColumnDefinition(colName) : void 0;
973
+ if (authDefinition && !isIdProperty(propName, prop, collection)) {
974
+ columns.push(` "${colName}" ${authDefinition}`);
975
+ return;
976
+ }
977
+ let colDef = ` "${colName}" ${getSqlColumnType(propName, prop, collection, collections)}`;
978
+ if (isIdProperty(propName, prop, collection)) colDef += " PRIMARY KEY";
979
+ if ("isId" in prop && prop.isId !== "manual" && prop.isId !== true && prop.isId !== "increment") {
980
+ if (prop.isId === "uuid") colDef += " DEFAULT gen_random_uuid()";
981
+ else if (prop.isId === "cuid") colDef += " DEFAULT cuid()";
982
+ else if (typeof prop.isId === "string") colDef += ` DEFAULT ${prop.isId}`;
983
+ }
984
+ if (!isIdProperty(propName, prop, collection) && prop.validation?.unique) colDef += " UNIQUE";
985
+ if (prop.type === "date") {
986
+ const dateProp = prop;
987
+ if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") colDef += " DEFAULT now()";
988
+ }
989
+ if (prop.validation?.required && !colDef.includes("PRIMARY KEY")) colDef += " NOT NULL";
990
+ columns.push(colDef);
991
+ }
992
+ });
993
+ if (isAuthCollection(collection)) {
994
+ const declared = new Set(Object.entries(collection.properties ?? {}).map(([name, prop]) => resolveColumnName(name, prop)));
995
+ for (const spec of AUTH_USERS_COLUMNS) {
996
+ if (declared.has(spec.column)) continue;
997
+ columns.push(` "${spec.column}" ${authUsersColumnSql(spec)}`);
998
+ }
999
+ }
1000
+ const searchSpec = options.includeSearch === false ? void 0 : buildSearchColumnSpec(collection);
1001
+ if (searchSpec) {
1002
+ columns.push(` ${searchColumnDefinition(searchSpec)}`);
1003
+ const fuzzyDef = fuzzyColumnDefinition(searchSpec);
1004
+ if (fuzzyDef) columns.push(` ${fuzzyDef}`);
1005
+ indexStatements.push(...searchIndexStatements(searchSpec));
1006
+ }
1007
+ if (options.includeVector !== false) {
1008
+ const vectorPlan = buildVectorIndexPlan(collection, resolveColumnName);
1009
+ indexStatements.push(...vectorIndexStatements(vectorPlan));
1010
+ for (const skip of vectorPlan.skipped) indexStatements.push(`-- No ANN index on "${skip.schema}"."${skip.table}"."${skip.column}": ${skip.reason}`);
1011
+ }
1012
+ indexStatements.push(...collectionIndexStatements(buildCollectionIndexSpecs(collection, resolveColumnName)));
1013
+ if (!columns.some((c) => c.includes("PRIMARY KEY"))) columns.unshift(" \"id\" TEXT PRIMARY KEY");
1014
+ ddl += columns.join(",\n");
1015
+ ddl += `\n);\n\n`;
1016
+ if (options.includePolicies) {
1017
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
1018
+ ddl += `\n`;
1019
+ const securityRules = getEffectiveSecurityRules(collection);
1020
+ if (securityRules.length > 0) {
1021
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
1022
+ securityRules.forEach((rule) => {
1023
+ policyStatements.push(generatePolicyDdl(collection, rule, resolveCollection));
1024
+ });
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ if (indexStatements.length > 0) {
1030
+ ddl += "-- Indexes\n";
1031
+ ddl += indexStatements.join("\n") + "\n\n";
1032
+ }
1033
+ if (fkStatements.length > 0) {
1034
+ ddl += "-- Foreign Key Constraints\n";
1035
+ ddl += fkStatements.join("\n") + "\n\n";
1036
+ }
1037
+ if (policyStatements.length > 0) {
1038
+ ddl += "-- Row Level Security Policies\n";
1039
+ ddl += policyStatements.join("");
1040
+ ddl += "\n";
1041
+ }
1042
+ return ddl;
1043
+ };
1044
+ var schemaOfCollection = (collection) => isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
1045
+ var bareTableName = (name) => name.includes(".") ? name.split(".").pop() : name;
1046
+ /**
1047
+ * Truncate a derived identifier the way Postgres does: to 63 bytes, silently.
1048
+ *
1049
+ * This is not cosmetic, and not really a naming choice at all — it is agreeing
1050
+ * with the name the database ALREADY stored. `ADD CONSTRAINT` on a longer name
1051
+ * succeeds and records the truncated form, so the untruncated name this used to
1052
+ * derive matched nothing in the catalogue. Boot-ensure compares its planned
1053
+ * constraints against `readExistingSchema`, which reads catalogue names, so the
1054
+ * comparison could never hit: every boot re-issued `ADD CONSTRAINT` for the same
1055
+ * constraint, forever, and got "already exists" every time. Non-fatal (foreign
1056
+ * keys are the one action allowed to fail) and therefore permanent — an error in
1057
+ * the log on every restart of a project whose table and column names happened to
1058
+ * be long.
1059
+ *
1060
+ * Byte length, not string length: NAMEDATALEN is 64 bytes, and a multi-byte
1061
+ * character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
1062
+ */
1063
+ var foreignKeyPlan = (args) => {
1064
+ const constraintName = toPostgresIdentifier(`${args.table}_${args.column}_fkey`);
1065
+ const onUpdate = args.onUpdate ? ` ON UPDATE ${args.onUpdate.toUpperCase()}` : "";
1066
+ return {
1067
+ constraintName,
1068
+ schema: args.schema,
1069
+ table: args.table,
1070
+ column: args.column,
1071
+ targetSchema: args.targetSchema,
1072
+ targetTable: args.targetTable,
1073
+ targetColumn: args.targetColumn,
1074
+ sql: `ALTER TABLE "${args.schema}"."${args.table}" ADD CONSTRAINT "${constraintName}" FOREIGN KEY ("${args.column}") REFERENCES "${args.targetSchema}"."${args.targetTable}" ("${args.targetColumn}") ON DELETE ${args.onDelete.toUpperCase()}${onUpdate};`
1075
+ };
1076
+ };
1077
+ /**
1078
+ * The FK columns the declared collections own — one entry per `relation`
1079
+ * (`belongsTo` side) or `reference` property.
1080
+ *
1081
+ * Split out of {@link generatePostgresDdl} so the boot-time schema ensure can
1082
+ * create the same columns with the same names, types and constraints. Before
1083
+ * this it skipped them outright, which was survivable only because `db push`
1084
+ * always followed; on a managed tenant nothing follows, so a table arrived
1085
+ * without the column its own collection reads and wrote 400 on every insert.
1086
+ *
1087
+ * A relation whose target is not in the bundle yields no column at all (the
1088
+ * generator returns early on an unresolvable target); a `reference` whose target
1089
+ * is unknown yields the column without a constraint. Both mirror the generator
1090
+ * exactly — a divergence here is a schema fork between boot and `db push`.
1091
+ */
1092
+ var planRelationalColumns = (allCollections) => {
1093
+ const collections = relationalCollections(allCollections);
1094
+ const plans = [];
1095
+ for (const collection of collections) {
1096
+ const tableName = getTableName(collection);
1097
+ if (!tableName) continue;
1098
+ const schema = schemaOfCollection(collection);
1099
+ const table = bareTableName(tableName);
1100
+ for (const [propName, rawProp] of Object.entries(collection.properties ?? {})) {
1101
+ const prop = rawProp;
1102
+ if (prop.type === "relation") {
1103
+ const refProp = prop;
1104
+ const relInfo = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
1105
+ if (relInfo?.kind !== "belongsTo") continue;
1106
+ if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) continue;
1107
+ let targetCollection;
1108
+ try {
1109
+ targetCollection = relInfo.target();
1110
+ } catch {
1111
+ continue;
1112
+ }
1113
+ if (!targetCollection) continue;
1114
+ const required = prop.validation?.required;
1115
+ const relationName = refProp.relation?.relationName ?? propName;
1116
+ const legacyKey = legacyForeignKeyName(relationName);
1117
+ const derived = relInfo.localKey === generateForeignKeyName(relationName);
1118
+ plans.push({
1119
+ schema,
1120
+ table,
1121
+ column: relInfo.localKey,
1122
+ legacyColumn: derived && legacyKey !== relInfo.localKey ? legacyKey : void 0,
1123
+ type: getSqlColumnType(propName, prop, collection, collections),
1124
+ foreignKey: foreignKeyPlan({
1125
+ schema,
1126
+ table,
1127
+ column: relInfo.localKey,
1128
+ targetSchema: schemaOfCollection(targetCollection),
1129
+ targetTable: bareTableName(getTableName(targetCollection)),
1130
+ targetColumn: getPrimaryKeyName(targetCollection),
1131
+ onDelete: relInfo.onDelete ?? defaultBelongsToOnDelete(required),
1132
+ onUpdate: relInfo.onUpdate
1133
+ })
1134
+ });
1135
+ } else if (prop.type === "reference") {
1136
+ const refProp = prop;
1137
+ const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
1138
+ const column = resolveColumnName(propName, prop);
1139
+ const type = getSqlColumnType(propName, prop, collection, collections);
1140
+ const required = prop.validation?.required;
1141
+ plans.push({
1142
+ schema,
1143
+ table,
1144
+ column,
1145
+ type,
1146
+ foreignKey: targetCollection ? foreignKeyPlan({
1147
+ schema,
1148
+ table,
1149
+ column,
1150
+ targetSchema: schemaOfCollection(targetCollection),
1151
+ targetTable: bareTableName(getTableName(targetCollection)),
1152
+ targetColumn: getPrimaryKeyName(targetCollection),
1153
+ onDelete: defaultBelongsToOnDelete(required)
1154
+ }) : void 0
1155
+ });
1156
+ }
1157
+ }
1158
+ }
1159
+ return plans;
1160
+ };
1161
+ /**
1162
+ * The junction tables a bundle's many-to-many relations imply.
1163
+ *
1164
+ * Derived from {@link resolveJunctionSpecs}, the same source the junction RLS
1165
+ * comes from, so a table created here always has policies planned for it — a
1166
+ * junction with row-level security left off is readable and writable by every
1167
+ * signed-in user, which is why the two must ship together.
1168
+ */
1169
+ var planJunctionTables = (allCollections) => {
1170
+ const collections = relationalCollections(allCollections);
1171
+ const plans = [];
1172
+ for (const spec of resolveJunctionSpecs(collections).values()) {
1173
+ const [source, target] = spec.endpoints;
1174
+ const legacyFor = (endpoint) => {
1175
+ const slug = toSnakeCase(endpoint.collection.slug ?? endpoint.collection.name ?? "");
1176
+ const legacy = legacyForeignKeyName(slug);
1177
+ return endpoint.junctionColumn === generateForeignKeyName(slug) && legacy !== endpoint.junctionColumn ? legacy : void 0;
1178
+ };
1179
+ const columns = [{
1180
+ name: source.junctionColumn,
1181
+ type: junctionKeyType(source.collection),
1182
+ legacyName: legacyFor(source)
1183
+ }, {
1184
+ name: target.junctionColumn,
1185
+ type: junctionKeyType(target.collection),
1186
+ legacyName: legacyFor(target)
1187
+ }];
1188
+ const onDelete = spec.declaringSides[0]?.relation.onDelete ?? "CASCADE";
1189
+ plans.push({
1190
+ schema: spec.schema,
1191
+ table: spec.table,
1192
+ columns,
1193
+ createTable: `CREATE TABLE IF NOT EXISTS "${spec.schema}"."${spec.table}" (` + columns.map((c) => `"${c.name}" ${c.type} NOT NULL`).join(", ") + `, PRIMARY KEY (${columns.map((c) => `"${c.name}"`).join(", ")}));`,
1194
+ foreignKeys: [source, target].map((endpoint, i) => foreignKeyPlan({
1195
+ schema: spec.schema,
1196
+ table: spec.table,
1197
+ column: columns[i].name,
1198
+ targetSchema: schemaOfCollection(endpoint.collection),
1199
+ targetTable: bareTableName(getTableName(endpoint.collection)),
1200
+ targetColumn: getPrimaryKeyName(endpoint.collection),
1201
+ onDelete
1202
+ }))
1203
+ });
1204
+ }
1205
+ return plans;
1206
+ };
1207
+ /**
1208
+ * The per-table RLS plan for the *declared* collections, as executable
1209
+ * statements — what the managed runtime applies at boot so a freshly
1210
+ * provisioned tenant database serves data instead of 401ing every read.
1211
+ *
1212
+ * Mirrors {@link generatePostgresPoliciesDdl} exactly (same
1213
+ * `generatePolicyStatements`, same enable-RLS, same effective rules, same
1214
+ * derived junction rules), so boot and `db push` produce identical policies from
1215
+ * identical collections.
1216
+ *
1217
+ * Junction tables are included, and have to be: boot creates them now
1218
+ * ({@link planJunctionTables}), and a junction with RLS left off is readable and
1219
+ * writable by every signed-in user. A junction whose table is still absent is
1220
+ * skipped by the applier, not planned away here.
1221
+ */
1222
+ var planCollectionPolicies = (allCollections) => {
1223
+ const collections = relationalCollections(allCollections);
1224
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
1225
+ const plans = [];
1226
+ const seen = /* @__PURE__ */ new Set();
1227
+ for (const collection of collections) {
1228
+ const tableName = getTableName(collection);
1229
+ if (!tableName) continue;
1230
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
1231
+ const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
1232
+ const qualified = `${schema}.${baseTableName}`;
1233
+ if (seen.has(qualified)) continue;
1234
+ seen.add(qualified);
1235
+ const policyStatements = [];
1236
+ for (const rule of getEffectiveSecurityRules(collection)) policyStatements.push(...generatePolicyStatements(collection, rule, resolveCollection));
1237
+ plans.push({
1238
+ schema,
1239
+ table: baseTableName,
1240
+ qualified,
1241
+ enableRls: `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;`,
1242
+ policyStatements
1243
+ });
1244
+ }
1245
+ for (const spec of resolveJunctionSpecs(collections).values()) {
1246
+ const qualified = `${spec.schema}.${spec.table}`;
1247
+ if (seen.has(qualified)) continue;
1248
+ seen.add(qualified);
1249
+ const junctionCollection = getJunctionCollectionConfig(spec);
1250
+ const policyStatements = [];
1251
+ for (const rule of getJunctionSecurityRules(spec)) policyStatements.push(...generatePolicyStatements(junctionCollection, rule, resolveCollection));
1252
+ plans.push({
1253
+ schema: spec.schema,
1254
+ table: spec.table,
1255
+ qualified,
1256
+ enableRls: `ALTER TABLE "${spec.schema}"."${spec.table}" ENABLE ROW LEVEL SECURITY;`,
1257
+ policyStatements
1258
+ });
1259
+ }
1260
+ return plans;
1261
+ };
1262
+ var generatePostgresPoliciesDdl = (allCollections) => {
1263
+ const collections = relationalCollections(allCollections);
1264
+ let ddl = "-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\n\n";
1265
+ const allTablesToGenerate = /* @__PURE__ */ new Map();
1266
+ for (const collection of collections) {
1267
+ const tableName = getTableName(collection);
1268
+ if (tableName) allTablesToGenerate.set(tableName, { collection });
1269
+ }
1270
+ for (const [tableName, { collection }] of allTablesToGenerate.entries()) {
1271
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
1272
+ const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
1273
+ ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
1274
+ ddl += `\n`;
1275
+ const securityRules = getEffectiveSecurityRules(collection);
1276
+ if (securityRules.length > 0) {
1277
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
1278
+ const injectedNames = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));
1279
+ securityRules.forEach((rule) => {
1280
+ if (rule.name && injectedNames.has(rule.name)) {
1281
+ ddl += `-- Injected by Rebase (not from this collection's securityRules).\n`;
1282
+ ddl += `-- Set \`disableDefaultPolicies: true\` on "${collection.slug}" to drop these and own its RLS outright.\n`;
1283
+ }
1284
+ ddl += generatePolicyDdl(collection, rule, resolveCollection);
1285
+ });
1286
+ ddl += "\n";
1287
+ }
1288
+ }
1289
+ const junctionSpecs = resolveJunctionSpecs(collections);
1290
+ for (const spec of junctionSpecs.values()) {
1291
+ ddl += `ALTER TABLE "${spec.schema}"."${spec.table}" ENABLE ROW LEVEL SECURITY;\n`;
1292
+ ddl += `\n`;
1293
+ const junctionRules = getJunctionSecurityRules(spec);
1294
+ if (junctionRules.length === 0) continue;
1295
+ const junctionCollection = getJunctionCollectionConfig(spec);
1296
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
1297
+ const declaringSlugs = spec.declaringSides.map((s) => s.collection.slug).join("\", \"");
1298
+ ddl += `-- Derived by Rebase for the junction "${spec.table}" (no collection declares it).\n`;
1299
+ ddl += `-- Reads require both endpoint rows to be visible; writes follow the update\n`;
1300
+ ddl += `-- rules of "${declaringSlugs}". Set \`disableDefaultPolicies: true\` on the\n`;
1301
+ ddl += `-- declaring collection(s) to drop these and police the junction yourself.\n`;
1302
+ junctionRules.forEach((rule) => {
1303
+ ddl += generatePolicyDdl(junctionCollection, rule, resolveCollection);
1304
+ });
1305
+ ddl += "\n";
1306
+ }
1307
+ return ddl;
1308
+ };
1309
+ //#endregion
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 };
1311
+
1312
+ //# sourceMappingURL=generate-postgres-ddl-logic-CtnpIBVv.js.map