@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g09316f6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
  2. package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +81 -7
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
  6. package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
  7. package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
  8. package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
  9. package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
  10. package/dist/cli-errors-C3g_kHBw.js.map +1 -0
  11. package/dist/cli-errors.d.ts +19 -0
  12. package/dist/cli-helpers.d.ts +47 -0
  13. package/dist/cli.js +233 -19
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
  16. package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
  17. package/dist/collections/validate-relations.d.ts +7 -0
  18. package/dist/column-plan-helpers-CpILzHJS.js +145 -0
  19. package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
  20. package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
  21. package/dist/doctor-D5SrGJ5P.js.map +1 -0
  22. package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
  23. package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
  24. package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
  25. package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
  26. package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
  27. package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
  28. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
  29. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
  30. package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
  31. package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
  33. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
  34. package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
  35. package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
  36. package/dist/history/HistoryService.d.ts +23 -4
  37. package/dist/index.es.js +1991 -580
  38. package/dist/index.es.js.map +1 -1
  39. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
  40. package/dist/plan-schema-Hgl62S-w.js +1531 -0
  41. package/dist/plan-schema-Hgl62S-w.js.map +1 -0
  42. package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
  43. package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
  44. package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
  45. package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
  46. package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
  47. package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
  48. package/dist/schema/catalogue-schema.d.ts +97 -0
  49. package/dist/schema/collection-index.d.ts +9 -0
  50. package/dist/schema/column-plan-helpers.d.ts +125 -0
  51. package/dist/schema/config-relations.d.ts +32 -0
  52. package/dist/schema/doctor-cli.js +4 -4
  53. package/dist/schema/doctor.d.ts +9 -0
  54. package/dist/schema/dynamic-tables.d.ts +27 -6
  55. package/dist/schema/ensure-collection-tables.d.ts +31 -268
  56. package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
  57. package/dist/schema/generate-drizzle-schema.js +1 -1
  58. package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
  59. package/dist/schema/generate-postgres-ddl.js +8 -2
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  61. package/dist/schema/generated-column-conflicts.d.ts +84 -0
  62. package/dist/schema/generated-schema-diff.d.ts +56 -0
  63. package/dist/schema/generated-schema-staleness.d.ts +33 -0
  64. package/dist/schema/introspect-db-logic.d.ts +2 -0
  65. package/dist/schema/plan/diff-plan.d.ts +220 -0
  66. package/dist/schema/plan/plan-schema.d.ts +84 -0
  67. package/dist/schema/plan/render-ddl.d.ts +62 -0
  68. package/dist/schema/plan/render-drizzle.d.ts +31 -0
  69. package/dist/schema/plan/types.d.ts +410 -0
  70. package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
  71. package/dist/schema/relation-names.d.ts +28 -0
  72. package/dist/schema/search-column.d.ts +9 -0
  73. package/dist/security/rls-enforcement.d.ts +17 -0
  74. package/dist/services/FetchService.d.ts +176 -36
  75. package/dist/services/PersistService.d.ts +17 -4
  76. package/dist/services/RelationService.d.ts +11 -3
  77. package/dist/services/collection-helpers.d.ts +16 -2
  78. package/dist/services/dataService.d.ts +29 -2
  79. package/dist/services/field-op-sql.d.ts +16 -0
  80. package/dist/services/field-viewer.d.ts +19 -0
  81. package/dist/services/realtimeService.d.ts +35 -1
  82. package/dist/services/row-pipeline.d.ts +29 -5
  83. package/dist/services/soft-delete.d.ts +73 -0
  84. package/dist/utils/pg-error-utils.d.ts +41 -1
  85. package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
  86. package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
  87. package/package.json +18 -10
  88. package/dist/cli-errors-DxN4yzdH.js.map +0 -1
  89. package/dist/doctor-D7HWmGzs.js.map +0 -1
  90. package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
  91. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
  92. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
  93. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
  94. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
  95. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
  96. package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
@@ -0,0 +1,1531 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { a as getPrimaryKeyName, c as isIdProperty, i as enumLabelsOf, l as resolveColumnName, o as getPrimaryKeyProp, r as declaresEnumType, s as idColumnDefault, t as assertSinglePrimaryKey } from "./column-plan-helpers-CpILzHJS.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 { c as vectorExtensionStatement, n as buildVectorColumnSpecs, o as vectorExtensionDeclared, r as buildVectorIndexPlan } from "./vector-index-CMJKb3C9.js";
6
+ import { a as deriveIndexName, t as buildCollectionIndexSpecs } from "./collection-index-DdnsxiJ_.js";
7
+ import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, REBASE_SCHEMA, hasForeignKeyOnTarget, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
8
+ import { TENANT_INDEX_REASON, fieldKeyForColumn, findRelation, getEffectiveSecurityRules, getEnumVarName, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getTableName, getTableVarName, getTenantConfig, 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 the schema plan's `PgType`: search only cares
99
+ * whether a value reaches text, and the mapping from property to *physical*
100
+ * type is asserted against the plan in the contract test rather than duplicated
101
+ * 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
+ /**
275
+ * Everything after the column name — the type and the generation expression.
276
+ *
277
+ * Split out because four emitters need it and only two of them have a place to
278
+ * put the name: `CREATE TABLE` and `ADD COLUMN` write `"col" <this>`, while the
279
+ * schema plan carries it as the column's SQL definition and the boot-time
280
+ * rebuild statement interpolates it on its own.
281
+ */
282
+ var searchColumnTypeSql = (expression, kind) => `${kind} GENERATED ALWAYS AS (${expression}) STORED`;
283
+ /** The column definition as it appears inside `CREATE TABLE`. */
284
+ var searchColumnDefinition = (spec) => `"${spec.column}" ${searchColumnTypeSql(spec.expression, "tsvector")}`;
285
+ /** The fuzzy column definition, when the spec asks for one. */
286
+ var fuzzyColumnDefinition = (spec) => spec.fuzzy ? `"${spec.fuzzy.column}" ${searchColumnTypeSql(spec.fuzzy.expression, "text")}` : void 0;
287
+ /**
288
+ * Index statements for the spec.
289
+ *
290
+ * `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
291
+ * SQL file replayed as one unit — a migration, or `search.sql` — where a
292
+ * concurrent build is not allowed. The boot-time ensure path runs statement by
293
+ * statement against tables that are live and populated, and uses the
294
+ * concurrent form instead; see `ensureSearchColumns`.
295
+ */
296
+ var searchIndexStatements = (spec) => {
297
+ const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
298
+ 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);`);
299
+ return statements;
300
+ };
301
+ /**
302
+ * Marker on the comment of every generated search column this module creates.
303
+ *
304
+ * Versioned because the fingerprint below is only comparable against itself: a
305
+ * future change to how it is computed has to read as "not stamped by this
306
+ * version" rather than as drift on every existing column.
307
+ */
308
+ var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
309
+ /**
310
+ * A stable fingerprint of one generated column's expression.
311
+ *
312
+ * Why a stamp rather than reading the expression back: Postgres stores a
313
+ * generated column's expression *parsed*, and hands it back deparsed — casts
314
+ * made explicit, identifiers requoted, schema qualifications added or dropped
315
+ * according to `search_path`. Comparing that text to the text we generated
316
+ * would report drift on wording, and this comparison decides whether a boot
317
+ * refuses, so a false positive is an outage. The stamp is written by the same
318
+ * code that writes the column, so equality means what it says.
319
+ */
320
+ var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
321
+ /**
322
+ * The stamps for a spec's generated columns — one per column, never shared.
323
+ *
324
+ * Per column on purpose: turning `fuzzy` on adds a second column and changes
325
+ * nothing about the first, and a spec-wide fingerprint would report the
326
+ * untouched `tsvector` column as drifted and refuse a boot over a change that
327
+ * is purely additive.
328
+ */
329
+ var searchColumnStamps = (spec) => {
330
+ const stamp = (column, expression) => {
331
+ const fingerprint = searchExpressionFingerprint(expression);
332
+ return {
333
+ column,
334
+ expression,
335
+ fingerprint,
336
+ sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
337
+ };
338
+ };
339
+ const stamps = [stamp(spec.column, spec.expression)];
340
+ if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
341
+ return stamps;
342
+ };
343
+ /**
344
+ * The same drift check as the boot ensure, for the SQL file.
345
+ *
346
+ * Needed because {@link searchColumnStamps} would otherwise *launder* drift on
347
+ * the migration path: `ADD COLUMN IF NOT EXISTS` does nothing to a column that
348
+ * exists, so a re-generated `search.sql` would stamp a stale column with the
349
+ * new block's fingerprint and the next boot would find them in agreement.
350
+ * Guarding first means the file refuses instead — `rebase db push` is attended,
351
+ * and the operator reading the failure is the person who changed the block.
352
+ */
353
+ var searchStampGuards = (spec) => searchColumnStamps(spec).map((stamp) => {
354
+ const relation = quote(`"${spec.schema}"."${spec.table}"`);
355
+ return `DO $rebase_search$
356
+ DECLARE recorded text;
357
+ BEGIN
358
+ SELECT col_description(a.attrelid, a.attnum) INTO recorded
359
+ FROM pg_attribute a
360
+ WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote(stamp.column)} AND NOT a.attisdropped;
361
+ IF recorded LIKE ${quote(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote(stamp.fingerprint)} THEN
362
+ 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;
363
+ END IF;
364
+ END
365
+ $rebase_search$;`;
366
+ });
367
+ /**
368
+ * The index names the spec creates.
369
+ *
370
+ * Needed by name, not just by statement, so Atlas can be told to exclude them
371
+ * from its diff — see `searchExcludePatterns`.
372
+ */
373
+ var searchIndexNames = (spec) => spec.fuzzy ? [spec.indexName, spec.fuzzy.indexName] : [spec.indexName];
374
+ /**
375
+ * The generated column names a collection's search block adds, if any.
376
+ *
377
+ * These are physical columns on the table, so `SELECT *` returns them. They are
378
+ * an index in column form — a list of lexeme positions, or a concatenation of
379
+ * every searchable field on the row — and nothing outside the query planner has
380
+ * any use for them. Left in, every list response carries a second, larger copy
381
+ * of the row's text.
382
+ */
383
+ var searchColumnNames = (collection) => {
384
+ let spec;
385
+ try {
386
+ spec = buildSearchColumnSpec(collection);
387
+ } catch {
388
+ return [];
389
+ }
390
+ if (!spec) return [];
391
+ return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
392
+ };
393
+ /**
394
+ * True for a column whose type only ever holds a search index.
395
+ *
396
+ * Independent of any collection config on purpose: an introspected database
397
+ * (BaaS mode) can carry a `tsvector` column this framework never created —
398
+ * Pagila's `film.fulltext` is the canonical one — and it should not be returned
399
+ * to callers either. `isDerivedIndexColumn` already keeps such a column out of
400
+ * the *properties*; this keeps it out of the *rows*.
401
+ */
402
+ var isSearchIndexColumn = (column) => {
403
+ const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
404
+ return sqlType === "tsvector" || sqlType === "tsquery";
405
+ };
406
+ /**
407
+ * A drizzle select projection over `table` with the search columns dropped.
408
+ *
409
+ * Returns undefined when nothing needs dropping, so the common case keeps using
410
+ * a plain `select()` and this stays invisible in the generated SQL.
411
+ */
412
+ var visibleColumnProjection = (tableColumns, collection) => {
413
+ const excluded = excludedColumnNames(tableColumns, collection);
414
+ if (!tableColumns || excluded.length === 0) return void 0;
415
+ const projection = {};
416
+ for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
417
+ return projection;
418
+ };
419
+ /** The same exclusion as a drizzle `db.query` `columns` denylist. */
420
+ var hiddenColumnsOption = (tableColumns, collection) => {
421
+ const excluded = excludedColumnNames(tableColumns, collection);
422
+ if (excluded.length === 0) return void 0;
423
+ return Object.fromEntries(excluded.map((name) => [name, false]));
424
+ };
425
+ /**
426
+ * The columns to keep out of a response, by name.
427
+ *
428
+ * `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
429
+ * for anything that is not a real drizzle table — a stub in a test, a derived
430
+ * or nested path with no table behind it. Nothing to exclude is the right
431
+ * answer there, and it has to be an answer rather than a throw: this runs on
432
+ * the read path of every collection, opted in or not.
433
+ */
434
+ var excludedColumnNames = (tableColumns, collection) => {
435
+ if (!tableColumns || typeof tableColumns !== "object") return [];
436
+ const byName = new Set(collection ? searchColumnNames(collection) : []);
437
+ return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
438
+ };
439
+ //#endregion
440
+ //#region src/schema/relation-names.ts
441
+ /**
442
+ * The `relationName` both sides of a link must agree on.
443
+ *
444
+ * Drizzle pairs an owning `one()` with its inverse `many()` by this string and
445
+ * by nothing else, and the two sides are computed by different callers holding
446
+ * different collections — so the rule has to be derivable from either end. It
447
+ * is: name the link after the table that carries the foreign key and the field
448
+ * key that column is served under.
449
+ *
450
+ * owning (belongsTo) → `{thisTable}_{fieldKey(localKey)}`
451
+ * inverse (hasMany/hasOne) → `{targetTable}_{fieldKey(foreignKeyOnTarget)}`
452
+ *
453
+ * Both spellings of `jobs.company` produce `jobs_companyId`. A many-to-many is
454
+ * named through its junction wiring and a `via` chain is not a Drizzle relation
455
+ * at all, so both keep the local name.
456
+ *
457
+ * The field key rather than the column: the Drizzle object is keyed by the wire
458
+ * name (`fieldKeyForColumn` is the one definition of it), and a name built from
459
+ * the column would differ between a collection that declares `columnName` and
460
+ * one that does not, for the same link.
461
+ *
462
+ * **This is the one definition.** `generate-drizzle-schema-logic.ts` holds a
463
+ * private copy (`computeSharedRelationName`) that this was lifted from verbatim;
464
+ * the generator should import this instead, so the file it writes and the
465
+ * relations the runtime builds from the live catalogue cannot drift apart.
466
+ */
467
+ function sharedRelationName(relation, sourceCollection) {
468
+ const fallback = () => {
469
+ if (relation.relationName) return relation.relationName;
470
+ try {
471
+ return toSnakeCase(relation.target().slug);
472
+ } catch {
473
+ return relation.relationName;
474
+ }
475
+ };
476
+ if (relation.kind === "belongsTo") return `${getTableName(sourceCollection)}_${fieldKeyForColumn(sourceCollection, relation.localKey)}`;
477
+ if (relation.kind === "hasMany" || relation.kind === "hasOne") try {
478
+ const target = relation.target();
479
+ return `${getTableName(target)}_${fieldKeyForColumn(target, relation.foreignKeyOnTarget)}`;
480
+ } catch {
481
+ return fallback();
482
+ }
483
+ return fallback();
484
+ }
485
+ //#endregion
486
+ //#region src/schema/plan/updated-at-trigger.ts
487
+ /**
488
+ * `autoValue: "on_update"`, as a database trigger.
489
+ *
490
+ * ## Why a trigger and not only the driver
491
+ *
492
+ * The driver stamps `updated_at` on every write it makes, and it has to keep
493
+ * doing so: a `beforeSave` hook reads the row it is about to persist, and a
494
+ * value that only appears once Postgres has written it is a value the hook
495
+ * cannot see.
496
+ *
497
+ * But the driver is not the only writer. A seed script, a migration backfill, a
498
+ * `psql` session, an admin fixing one row by hand — every one of those leaves
499
+ * `updated_at` at whatever it was, and the column then says the row has not
500
+ * changed since a date that is simply wrong. Anything reading it to decide what
501
+ * to re-index, re-sync or re-send skips the row. The declaration says "this
502
+ * column tracks the last write"; only the database can make that true.
503
+ *
504
+ * So both: the app stamps so hooks see the value, and a `BEFORE UPDATE` trigger
505
+ * stamps so every other writer does too. They agree because they both mean
506
+ * "now" — the trigger's `now()` is the transaction's start time, which is the
507
+ * same instant the driver's own timestamp is written under.
508
+ *
509
+ * ## Why it is carved out of Atlas
510
+ *
511
+ * Atlas's free tier refuses to *parse* a desired-state file containing a
512
+ * function ("functions and procedures are available to logged-in users only"),
513
+ * and a trigger is a function plus a binding. This is the same arrangement the
514
+ * search helpers already have: excluded from Atlas's view by pattern, written
515
+ * to a file Rebase applies itself, applied at boot by the schema ensure, and
516
+ * appended to migrations by the CLI so a replay is complete.
517
+ *
518
+ * ## Why one function for every column
519
+ *
520
+ * The column name arrives as a trigger argument (`TG_ARGV[0]`), so a project
521
+ * with forty `updated_at` columns installs one function and forty bindings
522
+ * rather than forty near-identical functions. `jsonb_populate_record(NEW, …)`
523
+ * is the documented way to set a field of a row whose type is not known until
524
+ * run time; it takes `NEW` as the row template, so the value is cast back to
525
+ * whatever the column actually is — a `timestamptz`, or a `date` if that is
526
+ * what the property declared.
527
+ */
528
+ /** The one trigger function, qualified. A frozen derived name. */
529
+ var SET_UPDATED_AT_FN = `${REBASE_SCHEMA}.set_updated_at`;
530
+ /**
531
+ * `CREATE OR REPLACE FUNCTION`, so replaying it against a database that already
532
+ * has it is a no-op — this is emitted into a file that runs on every push and
533
+ * is appended to migrations that run against databases at any stage of life.
534
+ */
535
+ var setUpdatedAtFunction = () => `CREATE OR REPLACE FUNCTION ${SET_UPDATED_AT_FN}() RETURNS trigger\nLANGUAGE plpgsql AS \$\$
536
+ BEGIN
537
+ NEW := jsonb_populate_record(NEW, jsonb_build_object(TG_ARGV[0], now()));
538
+ RETURN NEW;
539
+ END;
540
+ \$\$;`;
541
+ /**
542
+ * `DROP TRIGGER IF EXISTS` before the `CREATE`.
543
+ *
544
+ * `CREATE OR REPLACE TRIGGER` exists only on Postgres 14+, and this runs
545
+ * against whatever a self-hosted project points at. Two statements rather than
546
+ * one is also what the boot-time applier needs: it issues DDL one statement at
547
+ * a time over the extended query protocol, which forbids multiple commands in
548
+ * one execute.
549
+ */
550
+ var dropTriggerStatement = (plan) => `DROP TRIGGER IF EXISTS "${plan.name}" ON "${plan.schema}"."${plan.table}";`;
551
+ var createTriggerStatement = (plan) => `CREATE TRIGGER "${plan.name}" BEFORE UPDATE ON "${plan.schema}"."${plan.table}" FOR EACH ROW EXECUTE FUNCTION ${SET_UPDATED_AT_FN}('${plan.column.replace(/'/g, "''")}');`;
552
+ /** Both statements, in the order they must run. */
553
+ var triggerStatements = (plan) => [dropTriggerStatement(plan), createTriggerStatement(plan)];
554
+ //#endregion
555
+ //#region src/schema/plan/plan-schema.ts
556
+ /**
557
+ * The only place a `Property` is read for schema purposes.
558
+ *
559
+ * Everything that used to `switch (prop.type)` — the Drizzle generator, the DDL
560
+ * generator, the boot-time ensure — now renders the {@link SchemaPlan} this
561
+ * produces. See `./types.ts` for why.
562
+ *
563
+ * Pure and synchronous. It reads collections and nothing else: no database, no
564
+ * filesystem, no process-wide registry. The one fact about the world it needs
565
+ * (which extensions a project's databases gave Rebase leave to install) is
566
+ * passed in, because a plan that depended on whatever module happened to be
567
+ * imported would not be comparable with itself.
568
+ *
569
+ * It **throws** rather than guessing. A configuration no renderer can honour —
570
+ * an empty enum, two `isId` properties, `isId: "cuid"`, an unknown
571
+ * `columnType`, a relation to a collection that is not in the bundle, a
572
+ * `search` block on a collection Postgres does not store — used to fail
573
+ * differently in each of the three emitters, or not at all, and the failure
574
+ * landed at `CREATE TABLE` time on a managed tenant with nobody in the loop.
575
+ * Here it lands once, with the collection and the property named.
576
+ */
577
+ /**
578
+ * Single-quote escaping for a SQL string literal (PostgreSQL doubles the
579
+ * quote). Enum labels and `defaultValue`s come straight from user-authored
580
+ * collection config, so a value like `it's` closes the literal early and the
581
+ * whole generated file stops parsing at that statement.
582
+ */
583
+ var quoteSqlLiteral = (value) => `'${value.replace(/'/g, "''")}'`;
584
+ var schemaOf = (collection) => isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
585
+ var bareTableName = (name) => name.includes(".") ? name.split(".").pop() : name;
586
+ var describe = (collection) => collection.slug ?? collection.name ?? "(unnamed)";
587
+ /**
588
+ * The `ON DELETE` a foreign key gets when the author did not say.
589
+ *
590
+ * One rule for every property that emits one — `belongsTo` relations and
591
+ * `reference` properties alike. `reference` kept its own `CASCADE` default for
592
+ * a release after this rule was written, which is exactly the split this
593
+ * function exists to prevent: two spellings of the same link, two
594
+ * data-retention behaviours, and only one of them documented.
595
+ *
596
+ * An optional link is `SET NULL`: the column can hold NULL, so dropping the
597
+ * parent leaves the child row with an empty pointer, which is what "optional"
598
+ * already means.
599
+ *
600
+ * A required link is **`RESTRICT`**, not `CASCADE`. `NOT NULL` says the child
601
+ * cannot exist without a parent; it does not say deleting the parent should
602
+ * take the child with it. That second claim is a data-retention decision, and
603
+ * defaulting to it meant `onDelete` — a field nobody has to write — silently
604
+ * turned every `DELETE FROM authors` into a cascade through posts, comments and
605
+ * anything else that hung off them.
606
+ */
607
+ var defaultBelongsToOnDelete = (required) => required ? "RESTRICT" : "SET NULL";
608
+ var foreignKeyPlan = (args) => {
609
+ const constraintName = toPostgresIdentifier(`${args.table}_${args.column}_fkey`);
610
+ const onUpdate = args.onUpdate ? ` ON UPDATE ${args.onUpdate.toUpperCase()}` : "";
611
+ return {
612
+ ...args,
613
+ constraintName,
614
+ 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}`
615
+ };
616
+ };
617
+ /**
618
+ * The type a column pointing at this collection's primary key must have — a
619
+ * junction endpoint, a `belongsTo` foreign key, a `reference`.
620
+ *
621
+ * One function because it was three: the ladder was spelled inline in
622
+ * `generatePostgresDdl`, again in `planJunctionTables` and a third time in the
623
+ * Drizzle generator.
624
+ */
625
+ var primaryKeyPgType = (collection) => {
626
+ const pk = getPrimaryKeyProp(collection);
627
+ if (pk.type === "number") return { kind: "integer" };
628
+ return pk.isUuid ? { kind: "uuid" } : { kind: "text" };
629
+ };
630
+ /** The `columnType`s a `number` property may name, and what each one is. */
631
+ var NUMBER_COLUMN_TYPES = {
632
+ "integer": { kind: "integer" },
633
+ "smallint": { kind: "smallint" },
634
+ "smallserial": { kind: "smallserial" },
635
+ "bigint": { kind: "bigint" },
636
+ "serial": { kind: "serial" },
637
+ "bigserial": { kind: "bigserial" },
638
+ "real": { kind: "real" },
639
+ "double precision": { kind: "doublePrecision" },
640
+ "numeric": { kind: "numeric" }
641
+ };
642
+ var numberType = (propName, prop, collection, isId) => {
643
+ if (prop.isId === "increment") return { kind: "integer" };
644
+ if (prop.columnType) {
645
+ const mapped = NUMBER_COLUMN_TYPES[prop.columnType];
646
+ if (!mapped) throw new Error(`Property "${propName}" of collection "${describe(collection)}" declares \`columnType: "${prop.columnType}"\`, which is not a Postgres numeric type Rebase emits. Use one of: ${Object.keys(NUMBER_COLUMN_TYPES).join(", ")}.`);
647
+ if (mapped.kind === "numeric") return numericType(prop);
648
+ return mapped;
649
+ }
650
+ if (prop.validation?.integer || isId) return { kind: "integer" };
651
+ return numericType(prop);
652
+ };
653
+ /** `NUMERIC`, with the precision and scale the property asks for. */
654
+ var numericType = (prop) => {
655
+ const { precision, scale } = prop;
656
+ if (precision === void 0) return { kind: "numeric" };
657
+ return scale === void 0 ? {
658
+ kind: "numeric",
659
+ precision
660
+ } : {
661
+ kind: "numeric",
662
+ precision,
663
+ scale
664
+ };
665
+ };
666
+ var stringType = (propName, prop, collection) => {
667
+ if (prop.enum) {
668
+ const labels = enumLabelsOf(propName, prop, collection);
669
+ const table = getTableName(collection);
670
+ const column = resolveColumnName(propName, prop);
671
+ return {
672
+ kind: "enum",
673
+ schema: schemaOf(collection),
674
+ name: `${table}_${column}`,
675
+ varName: getEnumVarName(table, propName),
676
+ labels
677
+ };
678
+ }
679
+ if (prop.isId === "uuid" || prop.columnType === "uuid") return { kind: "uuid" };
680
+ if (prop.columnType === "char") return {
681
+ kind: "char",
682
+ length: resolveStringColumnLength(prop)
683
+ };
684
+ if (prop.columnType === "varchar") return {
685
+ kind: "varchar",
686
+ length: resolveStringColumnLength(prop)
687
+ };
688
+ if (prop.columnType !== void 0 && prop.columnType !== "text") throw new Error(`Property "${propName}" of collection "${describe(collection)}" declares \`columnType: "${String(prop.columnType)}"\`, which is not a Postgres string type Rebase emits. Use one of: text, varchar, char, uuid.`);
689
+ return { kind: "text" };
690
+ };
691
+ var arrayElementType = (prop) => {
692
+ let colType = prop.columnType;
693
+ if (!colType && prop.of && !Array.isArray(prop.of)) {
694
+ const of = prop.of;
695
+ if (of.type === "string") colType = "text[]";
696
+ else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
697
+ else if (of.type === "boolean") colType = "boolean[]";
698
+ }
699
+ switch (colType) {
700
+ case "text[]": return { kind: "text" };
701
+ case "integer[]": return { kind: "integer" };
702
+ case "boolean[]": return { kind: "boolean" };
703
+ case "numeric[]": return { kind: "numeric" };
704
+ default: return;
705
+ }
706
+ };
707
+ /**
708
+ * The Postgres type a property's column has.
709
+ *
710
+ * Every member of `DataType` has an arm. A type this does not know is a
711
+ * generator that has not been taught it yet, and it says so — a silent `TEXT`
712
+ * here is how `geopoint` ended up with a database column, no Drizzle key, and
713
+ * every write to it discarded with a 201.
714
+ */
715
+ var columnPgType = (propName, prop, collection, resolveCollection) => {
716
+ switch (prop.type) {
717
+ case "string": return stringType(propName, prop, collection);
718
+ case "number": return numberType(propName, prop, collection, isIdProperty(propName, prop, collection));
719
+ case "boolean": return { kind: "boolean" };
720
+ case "date": {
721
+ const dateProp = prop;
722
+ if (dateProp.columnType === "date") return { kind: "date" };
723
+ if (dateProp.columnType === "time") return { kind: "time" };
724
+ return { kind: "timestamptz" };
725
+ }
726
+ case "map": return prop.columnType === "json" ? { kind: "json" } : { kind: "jsonb" };
727
+ case "geopoint": return { kind: "jsonb" };
728
+ case "array": {
729
+ const arrayProp = prop;
730
+ if (arrayProp.columnType === "json") return { kind: "json" };
731
+ const element = arrayElementType(arrayProp);
732
+ return element ? {
733
+ kind: "array",
734
+ of: element
735
+ } : { kind: "jsonb" };
736
+ }
737
+ case "vector": return {
738
+ kind: "vector",
739
+ dimensions: prop.dimensions
740
+ };
741
+ case "binary": return { kind: "bytea" };
742
+ case "relation":
743
+ case "reference": {
744
+ const target = linkTarget(propName, prop, collection, resolveCollection);
745
+ return target ? primaryKeyPgType(target) : { kind: "text" };
746
+ }
747
+ default: throw new Error(`No Postgres column type for property "${propName}" of type "${prop.type}" in collection "${describe(collection)}". Add a case to \`columnPgType\` in schema/plan/plan-schema.ts.`);
748
+ }
749
+ };
750
+ /** The collection a `relation` or `reference` points at, when it resolves. */
751
+ var linkTarget = (propName, prop, collection, resolveCollection) => {
752
+ if (prop.type === "reference") {
753
+ const path = prop.path;
754
+ return path ? resolveCollection(path) : void 0;
755
+ }
756
+ const relProp = prop;
757
+ const relation = findRelation(resolveCollectionRelations(collection), relProp.relation?.relationName ?? propName);
758
+ if (relation?.kind !== "belongsTo") return void 0;
759
+ try {
760
+ return relation.target();
761
+ } catch {
762
+ return;
763
+ }
764
+ };
765
+ /**
766
+ * A `defaultValue` as a database DEFAULT.
767
+ *
768
+ * `defaultValue` reached neither the schema nor — until recently — the write
769
+ * path, so a documented, type-checked field did nothing at all: the panel
770
+ * pre-filled it, an insert from anywhere else (the REST API, a seed, psql) did
771
+ * not, and the column came out NULL. A DEFAULT is the one place that binds
772
+ * every writer.
773
+ *
774
+ * Only literals. A `reference`'s default is an `EntityReference` object and a
775
+ * `vector`'s is an embedding — neither is a column literal, and inventing one
776
+ * would be a value the write path and the database disagree about.
777
+ * `undefined` for those, which is what "no DEFAULT" means everywhere here.
778
+ */
779
+ var literalDefault = (prop, type) => {
780
+ const value = prop.defaultValue;
781
+ if (value === void 0 || value === null) return void 0;
782
+ switch (prop.type) {
783
+ case "string": return typeof value === "string" ? {
784
+ kind: "literal",
785
+ value,
786
+ sql: quoteSqlLiteral(value)
787
+ } : void 0;
788
+ case "number": return typeof value === "number" && Number.isFinite(value) ? {
789
+ kind: "literal",
790
+ value,
791
+ sql: String(value)
792
+ } : void 0;
793
+ case "boolean": return typeof value === "boolean" ? {
794
+ kind: "literal",
795
+ value,
796
+ sql: value ? "TRUE" : "FALSE"
797
+ } : void 0;
798
+ case "date": {
799
+ const iso = value instanceof Date ? Number.isNaN(value.getTime()) ? void 0 : value.toISOString() : typeof value === "string" ? value : void 0;
800
+ return iso === void 0 ? void 0 : {
801
+ kind: "literal",
802
+ value: iso,
803
+ sql: quoteSqlLiteral(iso)
804
+ };
805
+ }
806
+ case "map":
807
+ case "array":
808
+ case "geopoint": {
809
+ if (type.kind !== "jsonb" && type.kind !== "json") return void 0;
810
+ let json;
811
+ try {
812
+ json = JSON.stringify(value);
813
+ } catch {
814
+ return;
815
+ }
816
+ if (json === void 0) return void 0;
817
+ return {
818
+ kind: "literal",
819
+ value,
820
+ sql: `${quoteSqlLiteral(json)}::${type.kind}`
821
+ };
822
+ }
823
+ default: return;
824
+ }
825
+ };
826
+ /** The DEFAULT a column carries: id strategy, then `autoValue`, then `defaultValue`. */
827
+ var columnDefault = (propName, prop, collection, type) => {
828
+ const idDefault = idColumnDefault(propName, prop, collection);
829
+ if (idDefault) {
830
+ if (idDefault.kind === "uuid") return {
831
+ kind: "sql",
832
+ expression: "gen_random_uuid()"
833
+ };
834
+ if (idDefault.kind === "identity") return { kind: "identity" };
835
+ return {
836
+ kind: "sql",
837
+ expression: idDefault.expression
838
+ };
839
+ }
840
+ if (prop.type === "date") {
841
+ const autoValue = prop.autoValue;
842
+ if (autoValue === "on_create" || autoValue === "on_update") return {
843
+ kind: "sql",
844
+ expression: "now()"
845
+ };
846
+ }
847
+ return literalDefault(prop, type);
848
+ };
849
+ /**
850
+ * One security rule, compiled to the clauses a policy is made of.
851
+ *
852
+ * The desugaring (`access` / `ownerField` / `roles` / structured condition /
853
+ * raw SQL → `PolicyExpression`) and the SQL compilation are `@rebasepro/common`'s,
854
+ * which is what the client-side evaluator uses too — so the UI, the DDL and the
855
+ * database agree about who can read a row. What lives here is only the shape:
856
+ * which operations a rule expands to, which clauses each operation takes, and
857
+ * the deny-all fallback for a clause that compiled to nothing.
858
+ */
859
+ var compileSecurityRule = (collection, rule, resolveCollection, injected, ruleKey = rule.name ?? "(unnamed)") => {
860
+ const tableName = getTableName(collection);
861
+ const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
862
+ const policyNames = getPolicyNamesForRule(rule, tableName);
863
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
864
+ return ops.map((operation, index) => {
865
+ const needsUsing = operation !== "insert";
866
+ const needsWithCheck = operation !== "select" && operation !== "delete";
867
+ let using = needsUsing && usingExpr ? policyToPostgres(usingExpr, collection, { resolveCollection }) : null;
868
+ let withCheck = needsWithCheck && withCheckExpr ? policyToPostgres(withCheckExpr, collection, { resolveCollection }) : null;
869
+ if (!using && needsUsing) using = "false";
870
+ if (!withCheck && needsWithCheck) withCheck = "false";
871
+ return {
872
+ name: policyNames[index],
873
+ ruleKey,
874
+ operation,
875
+ mode: rule.mode ?? "permissive",
876
+ roles: rule.pgRoles ? [...rule.pgRoles].sort() : ["public"],
877
+ using,
878
+ withCheck,
879
+ injected
880
+ };
881
+ });
882
+ };
883
+ function planSchema(allCollections, options = {}) {
884
+ assertSearchIsPostgresOnly(allCollections);
885
+ const collections = relationalCollections(allCollections);
886
+ collections.forEach(assertSinglePrimaryKey);
887
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
888
+ const declaredSchemas = Array.from(new Set(collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)));
889
+ const schemas = Array.from(/* @__PURE__ */ new Set([REBASE_SCHEMA, ...declaredSchemas]));
890
+ const enums = [];
891
+ const seenEnums = /* @__PURE__ */ new Set();
892
+ /**
893
+ * Every enum type one table's properties declare, appended once.
894
+ *
895
+ * `declaredSchema` is passed rather than read off the config because a
896
+ * junction's synthetic collection carries `schema: "public"` as a resolved
897
+ * fact and not as a declaration — emitting it as one would make the
898
+ * generated file reach for a `pgSchema("public")` variable that exists for
899
+ * nobody.
900
+ */
901
+ const collectEnumTypes = (collection, properties, declaredSchema) => {
902
+ for (const [propName, rawProp] of Object.entries(properties)) {
903
+ const prop = rawProp;
904
+ if (!("enum" in prop) || !prop.enum) continue;
905
+ if (prop.type !== "string" && prop.type !== "number") continue;
906
+ const labels = enumLabelsOf(propName, prop, collection);
907
+ if (!declaresEnumType(prop)) continue;
908
+ const schema = schemaOf(collection);
909
+ const name = `${getTableName(collection)}_${resolveColumnName(propName, prop)}`;
910
+ const qualified = `${schema}.${name}`;
911
+ if (seenEnums.has(qualified)) continue;
912
+ seenEnums.add(qualified);
913
+ enums.push({
914
+ schema,
915
+ name,
916
+ qualified,
917
+ declaredSchema,
918
+ varName: getEnumVarName(getTableName(collection), propName),
919
+ labels
920
+ });
921
+ }
922
+ };
923
+ for (const collection of collections) collectEnumTypes(collection, collection.properties ?? {}, isPostgresCollectionConfig(collection) ? collection.schema : void 0);
924
+ const tableEntries = /* @__PURE__ */ new Map();
925
+ for (const collection of collections) {
926
+ const tableName = getTableName(collection);
927
+ if (tableName) tableEntries.set(tableName, { collection });
928
+ for (const relation of Object.values(resolveCollectionRelations(collection))) {
929
+ if (!isManyToMany(relation)) continue;
930
+ const junctionTable = relation.through.table;
931
+ if (tableEntries.has(junctionTable)) continue;
932
+ tableEntries.set(junctionTable, {
933
+ collection: {
934
+ table: junctionTable,
935
+ properties: {}
936
+ },
937
+ junction: {
938
+ relation,
939
+ source: collection
940
+ }
941
+ });
942
+ }
943
+ }
944
+ const junctionSpecs = resolveJunctionSpecs(collections);
945
+ for (const spec of junctionSpecs.values()) {
946
+ if (Object.keys(spec.properties).length === 0) continue;
947
+ collectEnumTypes(getJunctionCollectionConfig(spec), spec.properties, void 0);
948
+ }
949
+ const triggerFunctionNeeded = { value: false };
950
+ const tables = [];
951
+ for (const [tableName, entry] of tableEntries) tables.push(entry.junction ? planJunctionTable(tableName, entry.junction.relation, entry.junction.source, junctionSpecs, resolveCollection, triggerFunctionNeeded) : planCollectionTable(entry.collection, resolveCollection, triggerFunctionNeeded));
952
+ const extensions = [];
953
+ const functions = [];
954
+ const seenStatements = /* @__PURE__ */ new Set();
955
+ const add = (into, statement) => {
956
+ if (seenStatements.has(statement)) return;
957
+ seenStatements.add(statement);
958
+ into.push(statement);
959
+ };
960
+ for (const table of tables) {
961
+ if (!table.search) continue;
962
+ for (const statement of searchExtensionStatements(table.search)) add(extensions, statement);
963
+ }
964
+ if (vectorExtensionDeclared(options.databaseExtensions) && tables.some((t) => t.vectorColumns.length > 0)) add(extensions, vectorExtensionStatement());
965
+ for (const table of tables) {
966
+ if (!table.search) continue;
967
+ for (const statement of searchHelperFunctions(table.search)) add(functions, statement);
968
+ }
969
+ if (triggerFunctionNeeded.value) add(functions, setUpdatedAtFunction());
970
+ return {
971
+ schemas,
972
+ declaredSchemas,
973
+ enums,
974
+ tables,
975
+ relations: planRelations(collections, tableEntries),
976
+ extensions,
977
+ functions,
978
+ collections,
979
+ options
980
+ };
981
+ }
982
+ function planCollectionTable(collection, resolveCollection, triggerFunctionNeeded) {
983
+ const schema = schemaOf(collection);
984
+ const table = bareTableName(getTableName(collection));
985
+ const auth = isAuthCollection(collection);
986
+ const columns = [];
987
+ const triggers = [];
988
+ const relations = resolveCollectionRelations(collection);
989
+ for (const [propName, rawProp] of Object.entries(collection.properties ?? {})) {
990
+ const prop = rawProp;
991
+ if (prop.type === "relation") {
992
+ const column = planRelationColumn(propName, prop, collection, relations, schema, table);
993
+ if (column) columns.push(column);
994
+ continue;
995
+ }
996
+ if (prop.type === "reference") {
997
+ columns.push(planReferenceColumn(propName, prop, collection, resolveCollection, schema, table));
998
+ continue;
999
+ }
1000
+ columns.push(planPropertyColumn(propName, prop, {
1001
+ collection,
1002
+ resolveCollection,
1003
+ schema,
1004
+ table,
1005
+ auth,
1006
+ triggers,
1007
+ triggerFunctionNeeded
1008
+ }));
1009
+ }
1010
+ if (auth) {
1011
+ const declared = new Set(columns.map((c) => c.column));
1012
+ for (const spec of AUTH_USERS_COLUMNS) {
1013
+ if (declared.has(spec.column)) continue;
1014
+ columns.push({
1015
+ key: spec.column,
1016
+ column: spec.column,
1017
+ type: authColumnType(spec.type),
1018
+ nullable: spec.notNull !== true,
1019
+ primaryKey: false,
1020
+ unique: false,
1021
+ sqlDefinition: authUsersColumnSql(spec),
1022
+ source: {
1023
+ kind: "auth",
1024
+ slug: collection.slug
1025
+ }
1026
+ });
1027
+ }
1028
+ }
1029
+ const search = buildSearchColumnSpec(collection);
1030
+ if (search) {
1031
+ columns.push({
1032
+ key: search.column,
1033
+ column: search.column,
1034
+ type: { kind: "tsvector" },
1035
+ nullable: true,
1036
+ primaryKey: false,
1037
+ unique: false,
1038
+ generated: {
1039
+ expression: search.expression,
1040
+ stored: true
1041
+ },
1042
+ sqlDefinition: searchColumnTypeSql(search.expression, "tsvector"),
1043
+ source: {
1044
+ kind: "search",
1045
+ slug: collection.slug
1046
+ }
1047
+ });
1048
+ if (search.fuzzy) columns.push({
1049
+ key: search.fuzzy.column,
1050
+ column: search.fuzzy.column,
1051
+ type: { kind: "text" },
1052
+ nullable: true,
1053
+ primaryKey: false,
1054
+ unique: false,
1055
+ generated: {
1056
+ expression: search.fuzzy.expression,
1057
+ stored: true
1058
+ },
1059
+ sqlDefinition: searchColumnTypeSql(search.fuzzy.expression, "text"),
1060
+ source: {
1061
+ kind: "search",
1062
+ slug: collection.slug
1063
+ }
1064
+ });
1065
+ }
1066
+ if (!columns.some((c) => c.primaryKey)) columns.unshift({
1067
+ key: "id",
1068
+ column: "id",
1069
+ type: { kind: "text" },
1070
+ nullable: false,
1071
+ primaryKey: true,
1072
+ unique: false,
1073
+ source: {
1074
+ kind: "implicit-id",
1075
+ slug: collection.slug
1076
+ }
1077
+ });
1078
+ const injected = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));
1079
+ const policies = getEffectiveSecurityRules(collection).flatMap((rule, index) => compileSecurityRule(collection, rule, resolveCollection, Boolean(rule.name && injected.has(rule.name)), rule.name ?? `#${index}`));
1080
+ const tenantIndexes = applyTenantColumnEffects(collection, columns, schema, table);
1081
+ return {
1082
+ schema,
1083
+ table,
1084
+ qualified: `${schema}.${table}`,
1085
+ declaredSchema: isPostgresCollectionConfig(collection) ? collection.schema : void 0,
1086
+ varName: getTableVarName(getTableName(collection)),
1087
+ kind: "collection",
1088
+ slug: collection.slug,
1089
+ columns,
1090
+ primaryKey: columns.filter((c) => c.primaryKey).map((c) => c.column),
1091
+ indexes: [...buildCollectionIndexSpecs(collection, resolveColumnName), ...tenantIndexes],
1092
+ search,
1093
+ vector: buildVectorIndexPlan(collection, resolveColumnName),
1094
+ vectorColumns: buildVectorColumnSpecs(collection, resolveColumnName),
1095
+ policies,
1096
+ triggers,
1097
+ auth
1098
+ };
1099
+ }
1100
+ /**
1101
+ * One declared, non-linking property, as a column.
1102
+ *
1103
+ * Extracted from {@link planCollectionTable} the moment a second table needed
1104
+ * it: a `manyToMany`'s `through.properties` are columns on the junction, and
1105
+ * "declared exactly like a collection's properties" has to mean the same code
1106
+ * reads them, or it is a promise the next `columnType` or `defaultValue` change
1107
+ * quietly breaks. A junction payload column therefore gets the type, the
1108
+ * nullability, the default, the UNIQUE and the `on_update` trigger a collection
1109
+ * column with the same declaration gets — because it is the same function
1110
+ * answering.
1111
+ *
1112
+ * `relation` and `reference` do not come through here; they own a foreign key
1113
+ * and are planned by {@link planRelationColumn} / {@link planReferenceColumn}.
1114
+ * A junction payload may not declare either (config validation refuses it), so
1115
+ * this is the whole of a payload column.
1116
+ */
1117
+ function planPropertyColumn(propName, prop, ctx) {
1118
+ const { collection, resolveCollection, schema, table, auth } = ctx;
1119
+ const isId = isIdProperty(propName, prop, collection);
1120
+ const column = resolveColumnName(propName, prop);
1121
+ const type = columnPgType(propName, prop, collection, resolveCollection);
1122
+ const required = prop.validation?.required === true;
1123
+ const defaultValue = columnDefault(propName, prop, collection, type);
1124
+ const authDefinition = auth && !isId ? authUsersColumnDefinition(column) : void 0;
1125
+ const plan = {
1126
+ key: propName,
1127
+ column,
1128
+ type,
1129
+ nullable: !(isId || required),
1130
+ primaryKey: isId,
1131
+ unique: !isId && prop.validation?.unique === true,
1132
+ default: defaultValue,
1133
+ sqlDefinition: authDefinition,
1134
+ source: {
1135
+ kind: "property",
1136
+ propName,
1137
+ slug: collection.slug
1138
+ }
1139
+ };
1140
+ if (prop.type === "date" && prop.autoValue === "on_update") {
1141
+ plan.touchOnUpdate = true;
1142
+ ctx.triggerFunctionNeeded.value = true;
1143
+ ctx.triggers.push({
1144
+ schema,
1145
+ table,
1146
+ column,
1147
+ name: toPostgresIdentifier(`${table}_${column}_touch`)
1148
+ });
1149
+ }
1150
+ return plan;
1151
+ }
1152
+ /**
1153
+ * The two schema effects a `tenant` declaration has that a policy cannot state.
1154
+ *
1155
+ * The policy itself is injected as an ordinary `SecurityRule`
1156
+ * (`buildTenantSecurityRule`) and compiled above with every other rule, so
1157
+ * nothing here touches `policies`. What is left is the column:
1158
+ *
1159
+ * - **`NOT NULL`.** A row whose tenant is NULL belongs to nobody, and the
1160
+ * restrictive policy compares the column to the caller's tenant — a
1161
+ * comparison against NULL is never true. Such a row is therefore invisible to
1162
+ * every caller, forever, including the one who wrote it. The write path
1163
+ * refuses to create one (`TENANT_REQUIRED`); this is the same statement made
1164
+ * where a raw `INSERT`, a seed and a migration can also hear it.
1165
+ * - **An index.** The tenancy predicate is ANDed into *every* statement against
1166
+ * the table, so an unindexed tenant column turns every read into a sequential
1167
+ * scan filtered by tenant. It is the one index a tenant-scoped table cannot
1168
+ * be without, and the one people most reliably forget — which is the whole
1169
+ * argument for deriving it rather than documenting it.
1170
+ *
1171
+ * The index goes through `deriveIndexName` like every other index, so it is a
1172
+ * Rebase-managed name (`_ix_<fingerprint>`), it is in the Atlas diff, and the
1173
+ * doctor recognises it. Because that name is a fingerprint of the index's
1174
+ * semantics, an author who has *already* declared a plain btree index on the
1175
+ * tenant column derives byte-for-byte the same name — so the two are the same
1176
+ * object and this adds nothing rather than colliding.
1177
+ *
1178
+ * Throws when the field names no column. `planSchema` throws rather than
1179
+ * guessing, and the alternative here is a table with a tenancy policy over a
1180
+ * column that does not exist: `CREATE POLICY` fails, RLS stays enabled, and the
1181
+ * collection denies every row.
1182
+ */
1183
+ function applyTenantColumnEffects(collection, columns, schema, table) {
1184
+ const tenant = getTenantConfig(collection);
1185
+ if (!tenant) return [];
1186
+ const column = findTenantColumn(collection, columns, tenant.field);
1187
+ if (!column) throw new Error(`Collection "${describe(collection)}" declares \`tenant: { field: "${tenant.field}" }\`, and no property of that name reaches a column. \`tenant\` says what a column means; it does not create one. Declare the property, or name the one that holds the tenant id.`);
1188
+ if (column.primaryKey) return [];
1189
+ column.nullable = false;
1190
+ const withoutName = {
1191
+ schema,
1192
+ table,
1193
+ method: "btree",
1194
+ unique: false,
1195
+ keys: [{
1196
+ column: column.column,
1197
+ direction: "asc",
1198
+ nulls: "last"
1199
+ }],
1200
+ include: [],
1201
+ predicate: null,
1202
+ reason: TENANT_INDEX_REASON
1203
+ };
1204
+ return [{
1205
+ ...withoutName,
1206
+ indexName: deriveIndexName(withoutName)
1207
+ }];
1208
+ }
1209
+ /**
1210
+ * The planned column a `tenant.field` names.
1211
+ *
1212
+ * Not simply `columns.find(c => c.key === field)`: a `belongsTo` relation
1213
+ * property emits its column under the *foreign key's* field name, so
1214
+ * `tenant: { field: "org" }` beside `org: { type: "relation", … }` has to
1215
+ * resolve through the relation to `org_id`. Matching only on `key` found
1216
+ * nothing there and the declaration would have been refused as naming no
1217
+ * column — for the shape the documentation recommends.
1218
+ */
1219
+ function findTenantColumn(collection, columns, field) {
1220
+ const direct = columns.find((c) => c.key === field);
1221
+ if (direct) return direct;
1222
+ const prop = collection.properties?.[field];
1223
+ if (prop?.type === "relation") {
1224
+ const relation = findRelation(resolveCollectionRelations(collection), prop.relation?.relationName ?? field);
1225
+ if (relation?.kind === "belongsTo" && relation.localKey) return columns.find((c) => c.column === relation.localKey);
1226
+ return;
1227
+ }
1228
+ if (!prop) return void 0;
1229
+ const columnName = resolveColumnName(field, prop);
1230
+ return columns.find((c) => c.column === columnName);
1231
+ }
1232
+ /**
1233
+ * The column a `belongsTo` relation owns, or `undefined` when the relation puts
1234
+ * no column on this table.
1235
+ *
1236
+ * Every other kind is a column on the target, a junction row, or a join chain.
1237
+ */
1238
+ function planRelationColumn(propName, prop, collection, relations, schema, table) {
1239
+ const relation = findRelation(relations, prop.relation?.relationName ?? propName);
1240
+ if (relation?.kind !== "belongsTo") return void 0;
1241
+ let target;
1242
+ try {
1243
+ target = relation.target();
1244
+ } catch {
1245
+ return;
1246
+ }
1247
+ if (!target) return void 0;
1248
+ const required = prop.validation?.required === true;
1249
+ const fkFieldKey = fieldKeyForColumn(collection, relation.localKey);
1250
+ const columnOwnedByProperty = Boolean(collection.properties?.[fkFieldKey]) && propName !== fkFieldKey;
1251
+ const relationName = prop.relation?.relationName ?? propName;
1252
+ const legacyKey = legacyForeignKeyName(relationName);
1253
+ const derived = relation.localKey === generateForeignKeyName(relationName);
1254
+ return {
1255
+ key: fkFieldKey,
1256
+ column: relation.localKey,
1257
+ type: primaryKeyPgType(target),
1258
+ nullable: !required,
1259
+ primaryKey: false,
1260
+ unique: prop.validation?.unique === true,
1261
+ columnOwnedByProperty: columnOwnedByProperty || void 0,
1262
+ legacyColumn: derived && legacyKey !== relation.localKey ? legacyKey : void 0,
1263
+ foreignKey: foreignKeyPlan({
1264
+ schema,
1265
+ table,
1266
+ column: relation.localKey,
1267
+ targetSchema: schemaOf(target),
1268
+ targetTable: bareTableName(getTableName(target)),
1269
+ targetColumn: getPrimaryKeyName(target),
1270
+ onDelete: relation.onDelete ?? defaultBelongsToOnDelete(required),
1271
+ onUpdate: relation.onUpdate
1272
+ }),
1273
+ source: {
1274
+ kind: "relation",
1275
+ propName,
1276
+ slug: collection.slug
1277
+ }
1278
+ };
1279
+ }
1280
+ /** The column a `reference` property owns — a foreign key like any other. */
1281
+ function planReferenceColumn(propName, prop, collection, resolveCollection, schema, table) {
1282
+ const target = prop.path ? resolveCollection(prop.path) : void 0;
1283
+ const column = resolveColumnName(propName, prop);
1284
+ const required = prop.validation?.required === true;
1285
+ return {
1286
+ key: propName,
1287
+ column,
1288
+ type: target ? primaryKeyPgType(target) : { kind: "text" },
1289
+ nullable: !required,
1290
+ primaryKey: false,
1291
+ unique: prop.validation?.unique === true,
1292
+ foreignKey: target ? foreignKeyPlan({
1293
+ schema,
1294
+ table,
1295
+ column,
1296
+ targetSchema: schemaOf(target),
1297
+ targetTable: bareTableName(getTableName(target)),
1298
+ targetColumn: getPrimaryKeyName(target),
1299
+ onDelete: defaultBelongsToOnDelete(required)
1300
+ }) : void 0,
1301
+ source: {
1302
+ kind: "reference",
1303
+ propName,
1304
+ slug: collection.slug
1305
+ }
1306
+ };
1307
+ }
1308
+ /**
1309
+ * An auth-owned column's declared SQL type, as a tag.
1310
+ *
1311
+ * `auth-users-columns` states these as SQL because that is the grammar its three
1312
+ * creators write in; the plan needs the tag so nothing downstream has to parse a
1313
+ * type back apart. Deliberately exhaustive rather than a fallback: a column auth
1314
+ * adds with a type nobody mapped should say so here, not turn into `TEXT` in a
1315
+ * table the login flow reads.
1316
+ */
1317
+ var AUTH_COLUMN_TYPES = {
1318
+ "TEXT": { kind: "text" },
1319
+ "TEXT[]": {
1320
+ kind: "array",
1321
+ of: { kind: "text" }
1322
+ },
1323
+ "BOOLEAN": { kind: "boolean" },
1324
+ "JSONB": { kind: "jsonb" },
1325
+ "TIMESTAMP WITH TIME ZONE": { kind: "timestamptz" }
1326
+ };
1327
+ var authColumnType = (sqlType) => {
1328
+ const mapped = AUTH_COLUMN_TYPES[sqlType];
1329
+ if (!mapped) throw new Error(`\`auth-users-columns\` declares a column of type "${sqlType}", which the schema plan has no tag for. Add it to \`AUTH_COLUMN_TYPES\` in schema/plan/plan-schema.ts.`);
1330
+ return mapped;
1331
+ };
1332
+ function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCollection, triggerFunctionNeeded) {
1333
+ if (!isManyToMany(relation)) throw new Error(`Internal: junction table "${tableName}" was reached from a ${relation.kind} relation.`);
1334
+ const table = bareTableName(tableName);
1335
+ const schema = "public";
1336
+ const target = relation.target();
1337
+ const { sourceColumn, targetColumn } = relation.through;
1338
+ const onDelete = relation.onDelete ?? "CASCADE";
1339
+ const spec = junctionSpecs.get(table);
1340
+ const legacyFor = (collection, column) => {
1341
+ const slug = toSnakeCase(collection.slug ?? collection.name ?? "");
1342
+ const legacy = legacyForeignKeyName(slug);
1343
+ return column === generateForeignKeyName(slug) && legacy !== column ? legacy : void 0;
1344
+ };
1345
+ const endpoint = (collection, column) => ({
1346
+ key: column,
1347
+ column,
1348
+ type: primaryKeyPgType(collection),
1349
+ nullable: false,
1350
+ primaryKey: false,
1351
+ unique: false,
1352
+ legacyColumn: legacyFor(collection, column),
1353
+ foreignKey: foreignKeyPlan({
1354
+ schema,
1355
+ table,
1356
+ column,
1357
+ targetSchema: schemaOf(collection),
1358
+ targetTable: bareTableName(getTableName(collection)),
1359
+ targetColumn: getPrimaryKeyName(collection),
1360
+ onDelete
1361
+ }),
1362
+ source: { kind: "junction-key" }
1363
+ });
1364
+ const columns = [endpoint(source, sourceColumn), endpoint(target, targetColumn)];
1365
+ const triggers = [];
1366
+ if (spec && Object.keys(spec.properties).length > 0) {
1367
+ const junctionCollection = getJunctionCollectionConfig(spec);
1368
+ const keyColumns = /* @__PURE__ */ new Set([sourceColumn, targetColumn]);
1369
+ for (const [propName, rawProp] of Object.entries(spec.properties)) {
1370
+ const plan = planPropertyColumn(propName, rawProp, {
1371
+ collection: junctionCollection,
1372
+ resolveCollection,
1373
+ schema,
1374
+ table,
1375
+ auth: false,
1376
+ triggers,
1377
+ triggerFunctionNeeded
1378
+ });
1379
+ if (keyColumns.has(plan.column)) continue;
1380
+ columns.push(plan);
1381
+ }
1382
+ }
1383
+ const policies = spec ? getJunctionSecurityRules(spec).flatMap((rule, index) => compileSecurityRule(getJunctionCollectionConfig(spec), rule, resolveCollection, false, rule.name ?? `#${index}`)) : [];
1384
+ return {
1385
+ schema,
1386
+ table,
1387
+ qualified: `${schema}.${table}`,
1388
+ varName: getTableVarName(tableName),
1389
+ kind: "junction",
1390
+ declaringSlugs: spec?.declaringSides.map((side) => side.collection.slug) ?? [],
1391
+ columns,
1392
+ primaryKey: [sourceColumn, targetColumn],
1393
+ indexes: [],
1394
+ vectorColumns: [],
1395
+ policies,
1396
+ triggers,
1397
+ auth: false
1398
+ };
1399
+ }
1400
+ /**
1401
+ * The `relations(...)` entries the generated file carries.
1402
+ *
1403
+ * Both sides of a link must share a `relationName` or Drizzle cannot pair them;
1404
+ * `sharedRelationName` is the rule, and each side derives it independently.
1405
+ */
1406
+ function planRelations(collections, tableEntries) {
1407
+ const plans = [];
1408
+ for (const [tableName, entry] of tableEntries) {
1409
+ const tableVar = getTableVarName(tableName);
1410
+ if (entry.junction) {
1411
+ const { relation, source } = entry.junction;
1412
+ if (!isManyToMany(relation)) continue;
1413
+ const target = relation.target();
1414
+ const owningRelationName = relation.relationName ?? toSnakeCase(getTableName(target));
1415
+ let inverseRelationName;
1416
+ try {
1417
+ for (const targetRel of Object.values(resolveCollectionRelations(target))) if (targetRel.kind !== "belongsTo" && targetRel.cardinality === "many" && targetRel.relationName === owningRelationName) {
1418
+ inverseRelationName = targetRel.relationName;
1419
+ break;
1420
+ }
1421
+ } catch {}
1422
+ plans.push({
1423
+ tableVar,
1424
+ key: relation.through.sourceColumn,
1425
+ kind: "one",
1426
+ targetVar: getTableVarName(getTableName(source)),
1427
+ relationName: owningRelationName,
1428
+ fields: [relation.through.sourceColumn],
1429
+ references: [getPrimaryKeyName(source)]
1430
+ });
1431
+ plans.push({
1432
+ tableVar,
1433
+ key: relation.through.targetColumn,
1434
+ kind: "one",
1435
+ targetVar: getTableVarName(getTableName(target)),
1436
+ relationName: inverseRelationName ?? `${tableName}_${relation.through.targetColumn}`,
1437
+ fields: [relation.through.targetColumn],
1438
+ references: [getPrimaryKeyName(target)]
1439
+ });
1440
+ continue;
1441
+ }
1442
+ const collection = entry.collection;
1443
+ const emitted = /* @__PURE__ */ new Set();
1444
+ for (const [key, relation] of Object.entries(resolveCollectionRelations(collection))) {
1445
+ let target;
1446
+ try {
1447
+ target = relation.target();
1448
+ } catch {
1449
+ continue;
1450
+ }
1451
+ const targetVar = getTableVarName(getTableName(target));
1452
+ const relationName = sharedRelationName(relation, collection);
1453
+ const dedupe = `${relationName}::${relation.kind}`;
1454
+ if (emitted.has(dedupe)) continue;
1455
+ emitted.add(dedupe);
1456
+ switch (relation.kind) {
1457
+ case "belongsTo":
1458
+ plans.push({
1459
+ tableVar,
1460
+ key,
1461
+ kind: "one",
1462
+ targetVar,
1463
+ relationName,
1464
+ fields: [fieldKeyForColumn(collection, relation.localKey)],
1465
+ references: [getPrimaryKeyName(target)]
1466
+ });
1467
+ break;
1468
+ case "hasOne":
1469
+ plans.push({
1470
+ tableVar,
1471
+ key,
1472
+ kind: "one",
1473
+ targetVar
1474
+ });
1475
+ break;
1476
+ case "hasMany":
1477
+ plans.push({
1478
+ tableVar,
1479
+ key,
1480
+ kind: "many",
1481
+ targetVar,
1482
+ relationName
1483
+ });
1484
+ break;
1485
+ case "manyToMany":
1486
+ plans.push({
1487
+ tableVar,
1488
+ key,
1489
+ kind: "many",
1490
+ targetVar: getTableVarName(relation.through.table),
1491
+ relationName
1492
+ });
1493
+ break;
1494
+ case "via": break;
1495
+ }
1496
+ }
1497
+ for (const other of collections) {
1498
+ if (other.slug === collection.slug) continue;
1499
+ for (const otherRel of Object.values(resolveCollectionRelations(other))) {
1500
+ if (!hasForeignKeyOnTarget(otherRel)) continue;
1501
+ let otherTarget;
1502
+ try {
1503
+ otherTarget = otherRel.target();
1504
+ } catch {
1505
+ continue;
1506
+ }
1507
+ if (otherTarget.slug !== collection.slug) continue;
1508
+ const relationName = sharedRelationName(otherRel, other);
1509
+ const dedupe = `${relationName}::belongsTo`;
1510
+ if (emitted.has(dedupe)) continue;
1511
+ emitted.add(dedupe);
1512
+ const otherVar = getTableVarName(getTableName(other));
1513
+ const fieldKey = fieldKeyForColumn(collection, otherRel.foreignKeyOnTarget);
1514
+ plans.push({
1515
+ tableVar,
1516
+ key: `_synth_${otherVar}_${fieldKey}`,
1517
+ kind: "one",
1518
+ targetVar: otherVar,
1519
+ relationName,
1520
+ fields: [fieldKey],
1521
+ references: [otherRel.sourceKey ? fieldKeyForColumn(other, otherRel.sourceKey) : getPrimaryKeyName(other)]
1522
+ });
1523
+ }
1524
+ }
1525
+ }
1526
+ return plans;
1527
+ }
1528
+ //#endregion
1529
+ export { searchHelperFunctions as _, triggerStatements as a, searchStampGuards as b, SEARCH_TEXT_FN as c, fuzzyColumnDefinition as d, hiddenColumnsOption as f, searchExtensionStatements as g, searchColumnStamps as h, SET_UPDATED_AT_FN as i, SEARCH_UNACCENT_FN as l, searchColumnNames as m, planSchema as n, sharedRelationName as o, searchColumnDefinition as p, quoteSqlLiteral as r, SEARCH_STAMP_PREFIX as s, columnPgType as t, buildSearchColumnSpec as u, searchIndexNames as v, visibleColumnProjection as x, searchIndexStatements as y };
1530
+
1531
+ //# sourceMappingURL=plan-schema-Hgl62S-w.js.map