@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g56af4a4

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 (180) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
  2. package/dist/BranchService-Bsn2wfaj.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +166 -13
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +78 -14
  6. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  7. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  8. package/dist/backup/backup-cli.d.ts +14 -0
  9. package/dist/backup/backup-service.d.ts +2 -2
  10. package/dist/backup/pg-binary.d.ts +5 -0
  11. package/dist/{backup-cli-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
  12. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  13. package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
  14. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  15. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  16. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  17. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  18. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  19. package/dist/cli-errors.d.ts +93 -5
  20. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  21. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  22. package/dist/cli-helpers.d.ts +52 -12
  23. package/dist/cli-output.d.ts +16 -28
  24. package/dist/cli-push.d.ts +87 -0
  25. package/dist/cli.js +625 -190
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
  28. package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
  29. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  30. package/dist/connection-BPoNILnD.js.map +1 -0
  31. package/dist/connection.d.ts +9 -1
  32. package/dist/data-transformer.d.ts +0 -8
  33. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  34. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  35. package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
  36. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  37. package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
  38. package/dist/doctor-C4WiftHm.js.map +1 -0
  39. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  40. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  41. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  42. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  43. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
  44. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  45. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  46. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  47. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  48. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  49. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
  50. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  51. package/dist/generate-files-CdEckX6c.js +199 -0
  52. package/dist/generate-files-CdEckX6c.js.map +1 -0
  53. package/dist/generate-postgres-ddl-logic-B64O2oZF.js +152 -0
  54. package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
  55. package/dist/generated-sql.d.ts +34 -0
  56. package/dist/history/HistoryService.d.ts +38 -6
  57. package/dist/history/ensure-history-table.d.ts +42 -4
  58. package/dist/index.es.js +5557 -7612
  59. package/dist/index.es.js.map +1 -1
  60. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
  61. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  62. package/dist/introspect-db-queries-D6kAqReI.js +339 -0
  63. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  64. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  65. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  66. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
  67. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  68. package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
  69. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  70. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  71. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  72. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  73. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  74. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
  75. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  76. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
  77. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  78. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
  79. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  80. package/dist/schema/auth-schema.d.ts +172 -2
  81. package/dist/schema/boot-ddl.d.ts +47 -0
  82. package/dist/schema/classify-change.d.ts +29 -1
  83. package/dist/schema/collection-index.d.ts +45 -0
  84. package/dist/schema/column-plan-helpers.d.ts +38 -21
  85. package/dist/schema/deferred-index-build.d.ts +131 -0
  86. package/dist/schema/destructive-sql.d.ts +27 -1
  87. package/dist/schema/doctor-cli.js +110 -6
  88. package/dist/schema/doctor-cli.js.map +1 -1
  89. package/dist/schema/doctor-search-scale.d.ts +45 -0
  90. package/dist/schema/doctor.d.ts +53 -1
  91. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  92. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  93. package/dist/schema/enum-labels-kept.d.ts +26 -0
  94. package/dist/schema/generate-drizzle-schema.js +1 -1
  95. package/dist/schema/generate-files.d.ts +44 -0
  96. package/dist/schema/generate-next-step.d.ts +0 -13
  97. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  98. package/dist/schema/generate-postgres-ddl.js +10 -81
  99. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  100. package/dist/schema/generate-schema-commit.d.ts +12 -0
  101. package/dist/schema/introspect-db-logic.d.ts +43 -0
  102. package/dist/schema/introspect-db-queries.d.ts +12 -2
  103. package/dist/schema/introspect-db-search.d.ts +22 -0
  104. package/dist/schema/introspect-db-storage.d.ts +83 -0
  105. package/dist/schema/introspect-db-structure.d.ts +10 -0
  106. package/dist/schema/introspect-db.js +17 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migration-post-process.d.ts +34 -0
  109. package/dist/schema/plan/diff-plan.d.ts +127 -1
  110. package/dist/schema/plan/plan-schema.d.ts +16 -8
  111. package/dist/schema/plan/render-ddl.d.ts +6 -0
  112. package/dist/schema/plan/types.d.ts +45 -11
  113. package/dist/schema/push-index-prebuild.d.ts +25 -0
  114. package/dist/schema/push-legacy-renames.d.ts +20 -0
  115. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  116. package/dist/search-column-BM-GV6vH.js +442 -0
  117. package/dist/search-column-BM-GV6vH.js.map +1 -0
  118. package/dist/security/policy-drift.d.ts +1 -1
  119. package/dist/security/rls-enforcement.d.ts +39 -4
  120. package/dist/services/BranchService.d.ts +22 -1
  121. package/dist/services/FetchService.d.ts +63 -12
  122. package/dist/services/PersistService.d.ts +15 -0
  123. package/dist/services/RelationService.d.ts +106 -3
  124. package/dist/services/cdc/CdcListener.d.ts +26 -10
  125. package/dist/services/cdc/identity-columns.d.ts +19 -0
  126. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  127. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  128. package/dist/services/channel-history.d.ts +32 -5
  129. package/dist/services/collection-helpers.d.ts +26 -1
  130. package/dist/services/field-viewer.d.ts +15 -1
  131. package/dist/services/pg-notify-listener.d.ts +95 -6
  132. package/dist/services/realtimeService.d.ts +467 -53
  133. package/dist/services/row-pipeline.d.ts +16 -9
  134. package/dist/services/socket-liveness.d.ts +85 -0
  135. package/dist/services/soft-delete.d.ts +12 -0
  136. package/dist/services/sql-script.d.ts +275 -0
  137. package/dist/services/vector-iterative-scan.d.ts +40 -0
  138. package/dist/services/write-depth.d.ts +22 -0
  139. package/dist/services/write-transaction-scope.d.ts +15 -3
  140. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  141. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  142. package/dist/socket-opt-ins.d.ts +64 -0
  143. package/dist/source-DBDbBTL4.js +811 -0
  144. package/dist/source-DBDbBTL4.js.map +1 -0
  145. package/dist/tsx-loader.d.ts +14 -0
  146. package/dist/utils/drizzle-conditions.d.ts +63 -3
  147. package/dist/utils/error-cause.d.ts +7 -0
  148. package/dist/utils/pg-error-utils.d.ts +2 -4
  149. package/dist/utils/result-rows.d.ts +10 -0
  150. package/dist/websocket.d.ts +63 -18
  151. package/package.json +7 -7
  152. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  153. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  154. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  155. package/dist/cli-errors-B8qHg02P.js +0 -798
  156. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  157. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  158. package/dist/cli-output-CNdMql-L.js +0 -47
  159. package/dist/cli-output-CNdMql-L.js.map +0 -1
  160. package/dist/collection-index-DdnsxiJ_.js +0 -286
  161. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  162. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  163. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  164. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  165. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  166. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  167. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  168. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  169. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  170. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  171. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  172. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  173. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  174. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  175. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  176. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  177. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  178. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  179. package/dist/source-Br7L7GOI.js +0 -410
  180. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -1,446 +1,13 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { a as getPrimaryKeyColumn, c as idColumnDefault, i as enumLabelsOf, l as isIdProperty, o as getPrimaryKeyName, r as declaresEnumType, s as getPrimaryKeyProp, t as assertSinglePrimaryKey, u as resolveColumnName } from "./column-plan-helpers-DF-8dTVa.js";
4
- import { i as isAuthCollection, n as authUsersColumnDefinition, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
3
+ import { a as buildSearchColumnSpec, d as searchColumnTypeSql, f as searchExtensionStatements, i as assertSearchIsPostgresOnly, p as searchHelperFunctions } from "./search-column-BM-GV6vH.js";
4
+ import { a as getPrimaryKeyName, c as isIdProperty, d as sqlExpressionText, i as getPrimaryKeyColumn, l as primaryKeyPropertyNames, n as declaresEnumType, o as getPrimaryKeyProp, r as enumLabelsOf, s as idColumnDefault, u as resolveColumnName } from "./column-plan-helpers-drZIsSNf.js";
5
+ import { i as isAuthCollection, n as authUsersColumnDefinition, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
5
6
  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_MODE, DEFAULT_SEARCH_WEIGHT, REBASE_SCHEMA, hasForeignKeyOnTarget, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
8
- import { TENANT_INDEX_REASON, assertBeforeQueryIsPostgresOnly, 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 raw = rawTextSql({
183
- column,
184
- jsonPath: rest,
185
- kind: classified.kind
186
- });
187
- const textSql = normalize(raw, cfg.unaccent === true);
188
- const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;
189
- return {
190
- path,
191
- column,
192
- jsonPath: rest,
193
- kind: classified.kind,
194
- weight,
195
- sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
196
- textSql,
197
- foldedTextSql: normalize(raw, true)
198
- };
199
- };
200
- /**
201
- * Build the full spec for a collection, or undefined when it has not opted in.
202
- *
203
- * Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
204
- * at boot surface that as a startup failure — a search block that half-works is
205
- * worse than one that refuses.
206
- */
207
- var buildSearchColumnSpec = (collection) => {
208
- const cfg = getSearchConfig(collection);
209
- if (!cfg) return void 0;
210
- 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.`);
211
- const table = getTableName(collection);
212
- const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
213
- const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;
214
- 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.`);
215
- const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
216
- const seen = /* @__PURE__ */ new Set();
217
- for (const f of fields) {
218
- if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
219
- seen.add(f.path);
220
- }
221
- const mode = cfg.mode ?? DEFAULT_SEARCH_MODE;
222
- const extensions = [];
223
- if (cfg.unaccent || mode === "hybrid") extensions.push("unaccent");
224
- if (cfg.fuzzy) extensions.push("pg_trgm");
225
- const spec = {
226
- schema,
227
- table,
228
- column,
229
- language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,
230
- unaccent: cfg.unaccent === true,
231
- mode,
232
- fields,
233
- expression: fields.map((f) => f.sql).join(" || "),
234
- indexName: toPostgresIdentifier(`${table}_${column}_gin`),
235
- extensions
236
- };
237
- if (cfg.fuzzy) {
238
- const fuzzyColumn = `${column}_text`;
239
- 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.`);
240
- spec.fuzzy = {
241
- column: fuzzyColumn,
242
- expression: fields.map((f) => f.textSql).join(" || ' ' || "),
243
- indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
244
- threshold: cfg.fuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD
245
- };
246
- }
247
- return spec;
248
- };
249
- /**
250
- * The IMMUTABLE wrappers the generated expressions call.
251
- *
252
- * `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
253
- * than an error, and idempotent for the same reason every other boot-time DDL
254
- * statement here is.
255
- *
256
- * The bodies are stable built-ins wrapped in an immutable promise — see the
257
- * module comment for why that promise is sound. `STRICT` matters: it makes NULL
258
- * in mean NULL out without executing the body, which is what the `coalesce` at
259
- * each call site then absorbs.
260
- */
261
- var searchHelperFunctions = (spec) => {
262
- 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 $$;`];
263
- if (spec.unaccent || spec.mode === "hybrid") 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) $$;`);
264
- return statements;
265
- };
266
- /**
267
- * `CREATE EXTENSION` statements the spec's expressions depend on.
268
- *
269
- * `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
270
- * `CREATE EXTENSION` installs into the first schema on `search_path`, which
271
- * defaults to `"$user", public` — and the scaffold's database role is named
272
- * `rebase`, the same as the schema the generator creates one statement earlier.
273
- * So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
274
- * dictionary in `rebase`, and every reference to `public.unaccent` below fails
275
- * with "text search dictionary does not exist". Observed, not theorised.
276
- */
277
- var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
278
- /**
279
- * Everything after the column name — the type and the generation expression.
280
- *
281
- * Split out because four emitters need it and only two of them have a place to
282
- * put the name: `CREATE TABLE` and `ADD COLUMN` write `"col" <this>`, while the
283
- * schema plan carries it as the column's SQL definition and the boot-time
284
- * rebuild statement interpolates it on its own.
285
- */
286
- var searchColumnTypeSql = (expression, kind) => `${kind} GENERATED ALWAYS AS (${expression}) STORED`;
287
- /** The column definition as it appears inside `CREATE TABLE`. */
288
- var searchColumnDefinition = (spec) => `"${spec.column}" ${searchColumnTypeSql(spec.expression, "tsvector")}`;
289
- /** The fuzzy column definition, when the spec asks for one. */
290
- var fuzzyColumnDefinition = (spec) => spec.fuzzy ? `"${spec.fuzzy.column}" ${searchColumnTypeSql(spec.fuzzy.expression, "text")}` : void 0;
291
- /**
292
- * Index statements for the spec.
293
- *
294
- * `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
295
- * SQL file replayed as one unit — a migration, or `search.sql` — where a
296
- * concurrent build is not allowed. The boot-time ensure path runs statement by
297
- * statement against tables that are live and populated, and uses the
298
- * concurrent form instead; see `ensureSearchColumns`.
299
- */
300
- var searchIndexStatements = (spec) => {
301
- const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
302
- 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);`);
303
- return statements;
304
- };
305
- /**
306
- * Marker on the comment of every generated search column this module creates.
307
- *
308
- * Versioned because the fingerprint below is only comparable against itself: a
309
- * future change to how it is computed has to read as "not stamped by this
310
- * version" rather than as drift on every existing column.
311
- */
312
- var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
313
- /**
314
- * A stable fingerprint of one generated column's expression.
315
- *
316
- * Why a stamp rather than reading the expression back: Postgres stores a
317
- * generated column's expression *parsed*, and hands it back deparsed — casts
318
- * made explicit, identifiers requoted, schema qualifications added or dropped
319
- * according to `search_path`. Comparing that text to the text we generated
320
- * would report drift on wording, and this comparison decides whether a boot
321
- * refuses, so a false positive is an outage. The stamp is written by the same
322
- * code that writes the column, so equality means what it says.
323
- */
324
- var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
325
- /**
326
- * The stamps for a spec's generated columns — one per column, never shared.
327
- *
328
- * Per column on purpose: turning `fuzzy` on adds a second column and changes
329
- * nothing about the first, and a spec-wide fingerprint would report the
330
- * untouched `tsvector` column as drifted and refuse a boot over a change that
331
- * is purely additive.
332
- */
333
- var searchColumnStamps = (spec) => {
334
- const stamp = (column, expression) => {
335
- const fingerprint = searchExpressionFingerprint(expression);
336
- return {
337
- column,
338
- expression,
339
- fingerprint,
340
- sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
341
- };
342
- };
343
- const stamps = [stamp(spec.column, spec.expression)];
344
- if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
345
- return stamps;
346
- };
347
- /**
348
- * The same drift check as the boot ensure, for the SQL file.
349
- *
350
- * Needed because {@link searchColumnStamps} would otherwise *launder* drift on
351
- * the migration path: `ADD COLUMN IF NOT EXISTS` does nothing to a column that
352
- * exists, so a re-generated `search.sql` would stamp a stale column with the
353
- * new block's fingerprint and the next boot would find them in agreement.
354
- * Guarding first means the file refuses instead — `rebase db push` is attended,
355
- * and the operator reading the failure is the person who changed the block.
356
- */
357
- var searchStampGuards = (spec) => searchColumnStamps(spec).map((stamp) => {
358
- const relation = quote(`"${spec.schema}"."${spec.table}"`);
359
- return `DO $rebase_search$
360
- DECLARE recorded text;
361
- BEGIN
362
- SELECT col_description(a.attrelid, a.attnum) INTO recorded
363
- FROM pg_attribute a
364
- WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote(stamp.column)} AND NOT a.attisdropped;
365
- IF recorded LIKE ${quote(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote(stamp.fingerprint)} THEN
366
- 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;
367
- END IF;
368
- END
369
- $rebase_search$;`;
370
- });
371
- /**
372
- * The index names the spec creates.
373
- *
374
- * Needed by name, not just by statement, so Atlas can be told to exclude them
375
- * from its diff — see `searchExcludePatterns`.
376
- */
377
- var searchIndexNames = (spec) => spec.fuzzy ? [spec.indexName, spec.fuzzy.indexName] : [spec.indexName];
378
- /**
379
- * The generated column names a collection's search block adds, if any.
380
- *
381
- * These are physical columns on the table, so `SELECT *` returns them. They are
382
- * an index in column form — a list of lexeme positions, or a concatenation of
383
- * every searchable field on the row — and nothing outside the query planner has
384
- * any use for them. Left in, every list response carries a second, larger copy
385
- * of the row's text.
386
- */
387
- var searchColumnNames = (collection) => {
388
- let spec;
389
- try {
390
- spec = buildSearchColumnSpec(collection);
391
- } catch {
392
- return [];
393
- }
394
- if (!spec) return [];
395
- return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
396
- };
397
- /**
398
- * True for a column whose type only ever holds a search index.
399
- *
400
- * Independent of any collection config on purpose: an introspected database
401
- * (BaaS mode) can carry a `tsvector` column this framework never created —
402
- * Pagila's `film.fulltext` is the canonical one — and it should not be returned
403
- * to callers either. `isDerivedIndexColumn` already keeps such a column out of
404
- * the *properties*; this keeps it out of the *rows*.
405
- */
406
- var isSearchIndexColumn = (column) => {
407
- const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
408
- return sqlType === "tsvector" || sqlType === "tsquery";
409
- };
410
- /**
411
- * A drizzle select projection over `table` with the search columns dropped.
412
- *
413
- * Returns undefined when nothing needs dropping, so the common case keeps using
414
- * a plain `select()` and this stays invisible in the generated SQL.
415
- */
416
- var visibleColumnProjection = (tableColumns, collection) => {
417
- const excluded = excludedColumnNames(tableColumns, collection);
418
- if (!tableColumns || excluded.length === 0) return void 0;
419
- const projection = {};
420
- for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
421
- return projection;
422
- };
423
- /** The same exclusion as a drizzle `db.query` `columns` denylist. */
424
- var hiddenColumnsOption = (tableColumns, collection) => {
425
- const excluded = excludedColumnNames(tableColumns, collection);
426
- if (excluded.length === 0) return void 0;
427
- return Object.fromEntries(excluded.map((name) => [name, false]));
428
- };
429
- /**
430
- * The columns to keep out of a response, by name.
431
- *
432
- * `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
433
- * for anything that is not a real drizzle table — a stub in a test, a derived
434
- * or nested path with no table behind it. Nothing to exclude is the right
435
- * answer there, and it has to be an answer rather than a throw: this runs on
436
- * the read path of every collection, opted in or not.
437
- */
438
- var excludedColumnNames = (tableColumns, collection) => {
439
- if (!tableColumns || typeof tableColumns !== "object") return [];
440
- const byName = new Set(collection ? searchColumnNames(collection) : []);
441
- return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
442
- };
443
- //#endregion
7
+ import { d as foreignKeyIndexSpec, o as buildCollectionIndexSpecs, p as leadsWithColumn, u as deriveIndexName } from "./source-DBDbBTL4.js";
8
+ import { REBASE_SCHEMA, hasForeignKeyOnTarget, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
9
+ import { TENANT_INDEX_REASON, assertBeforeQueryIsPostgresOnly, compileRulePolicies, fieldKeyForColumn, findRelation, getEffectiveSecurityRules, getEnumVarName, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getTableName, getTableVarName, getTenantConfig, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, resolveStringColumnLength } from "@rebasepro/common";
10
+ import { generateForeignKeyName, getPolicySupersededNames, legacyForeignKeyName, toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
444
11
  //#region src/schema/relation-names.ts
445
12
  /**
446
13
  * The `relationName` both sides of a link must agree on.
@@ -571,7 +138,7 @@ var triggerStatements = (plan) => [dropTriggerStatement(plan), createTriggerStat
571
138
  * imported would not be comparable with itself.
572
139
  *
573
140
  * It **throws** rather than guessing. A configuration no renderer can honour —
574
- * an empty enum, two `isId` properties, `isId: "cuid"`, an unknown
141
+ * an empty enum, `isId: "cuid"`, a foreign key into a composite key, an unknown
575
142
  * `columnType`, a relation to a collection that is not in the bundle, a
576
143
  * `search` block on a collection Postgres does not store — used to fail
577
144
  * differently in each of the three emitters, or not at all, and the failure
@@ -588,6 +155,8 @@ var quoteSqlLiteral = (value) => `'${value.replace(/'/g, "''")}'`;
588
155
  var schemaOf = (collection) => isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
589
156
  var bareTableName = (name) => name.includes(".") ? name.split(".").pop() : name;
590
157
  var describe = (collection) => collection.slug ?? collection.name ?? "(unnamed)";
158
+ /** A link, as a refusal names it: `relation "posts.author"`. */
159
+ var linkLabel = (kind, collection, key) => `${kind} "${describe(collection)}.${key}"`;
591
160
  /**
592
161
  * The `ON DELETE` a foreign key gets when the author did not say.
593
162
  *
@@ -632,13 +201,22 @@ var foreignKeyPlan = (args) => {
632
201
  * One exception: a `serial` key owns a sequence, and a column pointing at it is
633
202
  * the plain integer of the same width — `SERIAL` there would give every
634
203
  * referencing column a sequence of its own.
204
+ *
205
+ * @param via the link asking, named if the collection's key is composite and so
206
+ * cannot be pointed at — see `getPrimaryKeyProp`.
635
207
  */
636
- var primaryKeyPgType = (collection) => {
637
- const { name, prop } = getPrimaryKeyProp(collection);
208
+ var primaryKeyPgType = (collection, via) => {
209
+ const { name, prop } = getPrimaryKeyProp(collection, via);
638
210
  if (prop?.type === "number") return withoutSequence(numberType(name, prop, collection, true));
639
211
  if (prop?.type === "string") return stringType(name, prop, collection);
640
212
  return { kind: "text" };
641
213
  };
214
+ /**
215
+ * The DEFAULT of the implicit `id TEXT PRIMARY KEY` a collection that declares
216
+ * no key gets: a uuid, as text. The same generator `isId: "uuid"` uses, cast to
217
+ * the column the key has always been.
218
+ */
219
+ var IMPLICIT_ID_DEFAULT = "gen_random_uuid()::text";
642
220
  var withoutSequence = (type) => {
643
221
  switch (type.kind) {
644
222
  case "smallserial": return { kind: "smallint" };
@@ -762,7 +340,7 @@ var columnPgType = (propName, prop, collection, resolveCollection) => {
762
340
  case "relation":
763
341
  case "reference": {
764
342
  const target = linkTarget(propName, prop, collection, resolveCollection);
765
- return target ? primaryKeyPgType(target) : { kind: "text" };
343
+ return target ? primaryKeyPgType(target, linkLabel(prop.type, collection, propName)) : { kind: "text" };
766
344
  }
767
345
  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.`);
768
346
  }
@@ -870,7 +448,10 @@ var literalDefault = (prop, type) => {
870
448
  default: return;
871
449
  }
872
450
  };
873
- /** The DEFAULT a column carries: id strategy, then `autoValue`, then `defaultValue`. */
451
+ /**
452
+ * The DEFAULT a column carries: id strategy, then `autoValue`, then a
453
+ * `databaseDefault` given as SQL, then `defaultValue`.
454
+ */
874
455
  var columnDefault = (propName, prop, collection, type) => {
875
456
  const idDefault = idColumnDefault(propName, prop, collection);
876
457
  if (idDefault) {
@@ -891,48 +472,62 @@ var columnDefault = (propName, prop, collection, type) => {
891
472
  expression: "now()"
892
473
  };
893
474
  }
475
+ if (typeof prop.databaseDefault === "string") {
476
+ const expression = sqlExpressionText(prop.databaseDefault).trim();
477
+ if (expression) return {
478
+ kind: "sql",
479
+ expression
480
+ };
481
+ }
894
482
  return literalDefault(prop, type);
895
483
  };
896
484
  /**
897
485
  * One security rule, compiled to the clauses a policy is made of.
898
486
  *
899
- * The desugaring (`access` / `ownerField` / `roles` / structured condition /
900
- * raw SQL → `PolicyExpression`) and the SQL compilation are `@rebasepro/common`'s,
901
- * which is what the client-side evaluator uses too — so the UI, the DDL and the
902
- * database agree about who can read a row. What lives here is only the shape:
903
- * which operations a rule expands to, which clauses each operation takes, and
904
- * the deny-all fallback for a clause that compiled to nothing.
487
+ * The compilation is `compileRulePolicies` in `@rebasepro/common`, which the
488
+ * Studio's RLS editor also compares the live database against — so the UI, the
489
+ * DDL and the database agree about who can read a row. What this adds is only
490
+ * the planner's bookkeeping: which rule a policy came from, and whether Rebase
491
+ * injected it.
905
492
  */
906
493
  var compileSecurityRule = (collection, rule, resolveCollection, injected, ruleKey = rule.name ?? "(unnamed)") => {
907
- const tableName = getTableName(collection);
908
- const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
909
- const policyNames = getPolicyNamesForRule(rule, tableName);
910
- const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
911
- return ops.map((operation, index) => {
912
- const needsUsing = operation !== "insert";
913
- const needsWithCheck = operation !== "select" && operation !== "delete";
914
- let using = needsUsing && usingExpr ? policyToPostgres(usingExpr, collection, { resolveCollection }) : null;
915
- let withCheck = needsWithCheck && withCheckExpr ? policyToPostgres(withCheckExpr, collection, { resolveCollection }) : null;
916
- if (!using && needsUsing) using = "false";
917
- if (!withCheck && needsWithCheck) withCheck = "false";
918
- return {
919
- name: policyNames[index],
920
- ruleKey,
921
- operation,
922
- mode: rule.mode ?? "permissive",
923
- roles: rule.pgRoles ? [...rule.pgRoles].sort() : ["public"],
924
- using,
925
- withCheck,
926
- injected
927
- };
494
+ const supersedes = getPolicySupersededNames(rule, getTableName(collection));
495
+ return compileRulePolicies(collection, rule, { resolveCollection }).map((policy, index) => ({
496
+ ...policy,
497
+ ruleKey,
498
+ injected,
499
+ ...index === 0 && supersedes.length > 0 ? { supersedes } : {}
500
+ }));
501
+ };
502
+ /**
503
+ * A table's policies, minus any superseded name Postgres would store one of
504
+ * them under.
505
+ *
506
+ * A superseded name is a 63-byte cut, and another rule's over-long name can
507
+ * cut to the very same bytes — `<table>_default_admin_read` and the old
508
+ * `<table>_default_admin_write_<op>` both do on a 50-byte table. Dropping it
509
+ * then would drop that policy right after its own `CREATE`. Its own `DROP
510
+ * POLICY IF EXISTS` already removes the old policy, so nothing is lost.
511
+ */
512
+ var withoutLiveSupersededNames = (policies) => {
513
+ const stored = new Set(policies.map((policy) => toPostgresIdentifier(policy.name)));
514
+ return policies.map((policy) => {
515
+ if (!policy.supersedes) return policy;
516
+ const supersedes = policy.supersedes.filter((name) => !stored.has(name));
517
+ if (supersedes.length === policy.supersedes.length) return policy;
518
+ const kept = { ...policy };
519
+ if (supersedes.length > 0) kept.supersedes = supersedes;
520
+ else delete kept.supersedes;
521
+ return kept;
928
522
  });
929
523
  };
930
524
  function planSchema(allCollections, options = {}) {
931
525
  assertSearchIsPostgresOnly(allCollections);
932
526
  assertBeforeQueryIsPostgresOnly(allCollections);
933
527
  const collections = relationalCollections(allCollections);
934
- collections.forEach(assertSinglePrimaryKey);
935
- const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
528
+ const firstBySlugOrTable = /* @__PURE__ */ new Map();
529
+ for (const c of collections) for (const key of [c.slug, getTableName(c)]) if (!firstBySlugOrTable.has(key)) firstBySlugOrTable.set(key, c);
530
+ const resolveCollection = (slug) => firstBySlugOrTable.get(slug);
936
531
  const declaredSchemas = Array.from(new Set(collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)));
937
532
  const schemas = Array.from(/* @__PURE__ */ new Set([REBASE_SCHEMA, ...declaredSchemas]));
938
533
  const enums = [];
@@ -1010,6 +605,7 @@ function planSchema(allCollections, options = {}) {
1010
605
  const triggerFunctionNeeded = { value: false };
1011
606
  const tables = [];
1012
607
  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));
608
+ assertNoTruncationCollisions(tables, enums);
1013
609
  const extensions = [];
1014
610
  const functions = [];
1015
611
  const seenStatements = /* @__PURE__ */ new Set();
@@ -1041,6 +637,31 @@ function planSchema(allCollections, options = {}) {
1041
637
  };
1042
638
  }
1043
639
  /**
640
+ * Refuse two relations — tables, junctions and enum types share one namespace
641
+ * per schema, because every table owns a row type of its own name — that are
642
+ * distinct in full but one name after Postgres keeps the first 63 bytes.
643
+ *
644
+ * `a_table_with_a_really_long_name_that_goes_on_and_on_and_on_forever` with an
645
+ * enum column and a many-to-many truncated its table, its enum type and its
646
+ * junction to the same identifier: the push failed with `type "…" already
647
+ * exists` and no validator mentioned the length. A name that already works is
648
+ * never touched — only a collision that cannot be created refuses.
649
+ */
650
+ function assertNoTruncationCollisions(tables, enums) {
651
+ const owners = /* @__PURE__ */ new Map();
652
+ const claim = (schema, name, what) => {
653
+ const key = `${schema}.${toPostgresIdentifier(name)}`;
654
+ const first = owners.get(key);
655
+ if (first === void 0) {
656
+ owners.set(key, what);
657
+ return;
658
+ }
659
+ throw new Error(`The ${what} and the ${first} both become "${schema}"."${toPostgresIdentifier(name)}" in Postgres, which keeps only the first 63 bytes of an identifier — and a table, a junction table and an enum type share one namespace (every table owns a row type of its name). The push would fail with "already exists". Give the collection a shorter \`table\`, or the enum's property a shorter \`columnName\` (an enum type is named <table>_<column>).`);
660
+ };
661
+ for (const table of tables) claim(table.schema, table.table, table.kind === "junction" ? `junction table "${table.table}"` : `table "${table.table}"`);
662
+ for (const e of enums) claim(e.schema, e.name, `enum type "${e.name}"`);
663
+ }
664
+ /**
1044
665
  * Refuse two many-to-many relations that resolve to one junction table with
1045
666
  * different key columns.
1046
667
  *
@@ -1156,22 +777,18 @@ function planCollectionTable(collection, resolveCollection, triggerFunctionNeede
1156
777
  }
1157
778
  });
1158
779
  }
1159
- if (!columns.some((c) => c.primaryKey)) columns.unshift({
1160
- key: "id",
1161
- column: "id",
1162
- type: { kind: "text" },
1163
- nullable: false,
1164
- primaryKey: true,
1165
- unique: false,
1166
- source: {
1167
- kind: "implicit-id",
1168
- slug: collection.slug
1169
- }
1170
- });
780
+ const primaryKey = planPrimaryKey(collection, columns);
1171
781
  const injected = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));
1172
- const policies = getEffectiveSecurityRules(collection).flatMap((rule, index) => compileSecurityRule(collection, rule, resolveCollection, Boolean(rule.name && injected.has(rule.name)), rule.name ?? `#${index}`));
1173
- const tenantIndexes = applyTenantColumnEffects(collection, columns, schema, table);
782
+ const policies = withoutLiveSupersededNames(getEffectiveSecurityRules(collection).flatMap((rule, index) => compileSecurityRule(collection, rule, resolveCollection, Boolean(rule.name && injected.has(rule.name)), rule.name ?? `#${index}`)));
783
+ const tenantIndexes = applyTenantColumnEffects(collection, columns, primaryKey, schema, table);
1174
784
  settleDefaultOnDelete(collection, columns, relations);
785
+ const indexes = [...buildCollectionIndexSpecs(collection, resolveColumnName), ...tenantIndexes];
786
+ indexes.push(...planForeignKeyIndexes(schema, table, columns, primaryKey, indexes, (column) => {
787
+ if (column.source.kind !== "relation" || !column.source.propName) return false;
788
+ const prop = collection.properties?.[column.source.propName];
789
+ const relation = findRelation(relations, prop?.relation?.relationName ?? column.source.propName);
790
+ return relation?.kind === "belongsTo" && relation.index;
791
+ }));
1175
792
  return {
1176
793
  schema,
1177
794
  table,
@@ -1181,8 +798,8 @@ function planCollectionTable(collection, resolveCollection, triggerFunctionNeede
1181
798
  kind: "collection",
1182
799
  slug: collection.slug,
1183
800
  columns,
1184
- primaryKey: columns.filter((c) => c.primaryKey).map((c) => c.column),
1185
- indexes: [...buildCollectionIndexSpecs(collection, resolveColumnName), ...tenantIndexes],
801
+ primaryKey,
802
+ indexes,
1186
803
  search,
1187
804
  vector: buildVectorIndexPlan(collection, resolveColumnName),
1188
805
  vectorColumns: buildVectorColumnSpecs(collection, resolveColumnName),
@@ -1192,6 +809,50 @@ function planCollectionTable(collection, resolveCollection, triggerFunctionNeede
1192
809
  };
1193
810
  }
1194
811
  /**
812
+ * The table's primary key, as the column names {@link TablePlan.primaryKey}
813
+ * lists — and the columns adjusted to state it one way only.
814
+ *
815
+ * One `isId` property is a key column of its own, declared inline
816
+ * (`"id" INTEGER PRIMARY KEY`, `.primaryKey()`), which is how
817
+ * {@link planPropertyColumn} planned it. Several are one composite key, and a
818
+ * composite key cannot be said column by column: two inline `PRIMARY KEY`
819
+ * clauses are two primary keys, which Postgres refuses. So each member stops
820
+ * being a key of its own, keeps NOT NULL (the constraint implies it, and every
821
+ * emitter spells it on the column so the three read the same), and the table
822
+ * gets one `PRIMARY KEY (a, b)` over the list returned here — the shape a
823
+ * junction's key has always had.
824
+ *
825
+ * A collection that declares no key gets an implicit `id TEXT PRIMARY KEY`,
826
+ * which is what `getPrimaryKeys` reads back.
827
+ */
828
+ function planPrimaryKey(collection, columns) {
829
+ const keyNames = primaryKeyPropertyNames(collection);
830
+ if (keyNames.length > 1) return keyNames.map((name) => {
831
+ const column = columns.find((c) => c.source.propName === name && !c.columnOwnedByProperty);
832
+ if (!column) throw new Error(`Property "${name}" of collection "${describe(collection)}" is marked \`isId\`, and it puts no column on this table, so it cannot be part of the table's primary key. Mark the property that holds the column instead.`);
833
+ column.primaryKey = false;
834
+ column.nullable = false;
835
+ return column.column;
836
+ });
837
+ if (!columns.some((c) => c.primaryKey)) columns.unshift({
838
+ key: "id",
839
+ column: "id",
840
+ type: { kind: "text" },
841
+ nullable: false,
842
+ primaryKey: true,
843
+ unique: false,
844
+ default: {
845
+ kind: "sql",
846
+ expression: IMPLICIT_ID_DEFAULT
847
+ },
848
+ source: {
849
+ kind: "implicit-id",
850
+ slug: collection.slug
851
+ }
852
+ });
853
+ return columns.filter((c) => c.primaryKey).map((c) => c.column);
854
+ }
855
+ /**
1195
856
  * One declared, non-linking property, as a column.
1196
857
  *
1197
858
  * Extracted from {@link planCollectionTable} the moment a second table needed
@@ -1274,12 +935,12 @@ function planPropertyColumn(propName, prop, ctx) {
1274
935
  * column that does not exist: `CREATE POLICY` fails, RLS stays enabled, and the
1275
936
  * collection denies every row.
1276
937
  */
1277
- function applyTenantColumnEffects(collection, columns, schema, table) {
938
+ function applyTenantColumnEffects(collection, columns, primaryKey, schema, table) {
1278
939
  const tenant = getTenantConfig(collection);
1279
940
  if (!tenant) return [];
1280
941
  const column = findTenantColumn(collection, columns, tenant.field);
1281
942
  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.`);
1282
- if (column.primaryKey) return [];
943
+ if (primaryKey[0] === column.column) return [];
1283
944
  column.nullable = false;
1284
945
  const withoutName = {
1285
946
  schema,
@@ -1301,6 +962,39 @@ function applyTenantColumnEffects(collection, columns, schema, table) {
1301
962
  }];
1302
963
  }
1303
964
  /**
965
+ * A btree on each foreign-key column of one table, where nothing already
966
+ * serves the lookup — see `foreignKeyIndexSpec` for why the column needs one.
967
+ *
968
+ * Skipped, so that no table ever carries two indexes doing one job, when the
969
+ * column already leads:
970
+ *
971
+ * - the primary key — `<table>_pkey` is a btree over the key in key order, so
972
+ * its first column is indexed and the rest are not (a junction's
973
+ * `(post_id, tag_id)` key serves `post_id`, and `tag_id` gets an index);
974
+ * - a UNIQUE on the column (`validation.unique`, on the relation or on the
975
+ * property that owns the column) — Postgres backs it with a btree;
976
+ * - another planned index that leads with it ({@link leadsWithColumn}) — one
977
+ * the collection declares in `indexes:`, or the tenant index.
978
+ *
979
+ * Only the plan is consulted, never the catalogue: `db push` renders this from
980
+ * the collections alone, and boot has to reach the same set with the same
981
+ * names, or Atlas would plan to drop what boot built.
982
+ */
983
+ function planForeignKeyIndexes(schema, table, columns, primaryKey, planned, wantsIndex) {
984
+ const created = columns.filter((c) => !c.columnOwnedByProperty);
985
+ const derived = [];
986
+ for (const column of columns) {
987
+ const fk = column.foreignKey;
988
+ if (!fk || !wantsIndex(column)) continue;
989
+ const name = fk.column;
990
+ if (primaryKey[0] === name) continue;
991
+ if (created.some((c) => c.column === name && c.unique)) continue;
992
+ if ([...planned, ...derived].some((spec) => leadsWithColumn(spec, name))) continue;
993
+ derived.push(foreignKeyIndexSpec(schema, table, name));
994
+ }
995
+ return derived;
996
+ }
997
+ /**
1304
998
  * The planned column a `tenant.field` names.
1305
999
  *
1306
1000
  * Not simply `columns.find(c => c.key === field)`: a `belongsTo` relation
@@ -1384,10 +1078,11 @@ function planRelationColumn(propName, prop, collection, relations, schema, table
1384
1078
  const relationName = prop.relation?.relationName ?? propName;
1385
1079
  const legacyKey = legacyForeignKeyName(relationName);
1386
1080
  const derived = relation.localKey === generateForeignKeyName(relationName);
1081
+ const via = linkLabel("relation", collection, propName);
1387
1082
  return {
1388
1083
  key: fkFieldKey,
1389
1084
  column: relation.localKey,
1390
- type: primaryKeyPgType(target),
1085
+ type: primaryKeyPgType(target, via),
1391
1086
  nullable: !required,
1392
1087
  primaryKey: false,
1393
1088
  unique: prop.validation?.unique === true,
@@ -1399,7 +1094,7 @@ function planRelationColumn(propName, prop, collection, relations, schema, table
1399
1094
  column: relation.localKey,
1400
1095
  targetSchema: schemaOf(target),
1401
1096
  targetTable: bareTableName(getTableName(target)),
1402
- targetColumn: getPrimaryKeyColumn(target),
1097
+ targetColumn: getPrimaryKeyColumn(target, via),
1403
1098
  onDelete: relation.onDelete ?? defaultBelongsToOnDelete(required),
1404
1099
  onUpdate: relation.onUpdate
1405
1100
  }),
@@ -1415,10 +1110,11 @@ function planReferenceColumn(propName, prop, collection, resolveCollection, sche
1415
1110
  const target = prop.path ? resolveCollection(prop.path) : void 0;
1416
1111
  const column = resolveColumnName(propName, prop);
1417
1112
  const required = prop.validation?.required === true;
1113
+ const via = linkLabel("reference", collection, propName);
1418
1114
  return {
1419
1115
  key: propName,
1420
1116
  column,
1421
- type: target ? primaryKeyPgType(target) : { kind: "text" },
1117
+ type: target ? primaryKeyPgType(target, via) : { kind: "text" },
1422
1118
  nullable: !required,
1423
1119
  primaryKey: false,
1424
1120
  unique: prop.validation?.unique === true,
@@ -1428,7 +1124,7 @@ function planReferenceColumn(propName, prop, collection, resolveCollection, sche
1428
1124
  column,
1429
1125
  targetSchema: schemaOf(target),
1430
1126
  targetTable: bareTableName(getTableName(target)),
1431
- targetColumn: getPrimaryKeyColumn(target),
1127
+ targetColumn: getPrimaryKeyColumn(target, via),
1432
1128
  onDelete: defaultBelongsToOnDelete(required)
1433
1129
  }) : void 0,
1434
1130
  source: {
@@ -1462,7 +1158,44 @@ var authColumnType = (sqlType) => {
1462
1158
  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.`);
1463
1159
  return mapped;
1464
1160
  };
1465
- function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCollection, triggerFunctionNeeded) {
1161
+ /**
1162
+ * Which declaring side a junction is planned from.
1163
+ *
1164
+ * With one side, that side — as it always was, so a junction only one end
1165
+ * declares keeps the key every database already has. With both ends declaring
1166
+ * it, the planner used to take whichever it reached first, and the two doors
1167
+ * reach them in different orders: `db push` loads files by name, boot walks the
1168
+ * bundle's array. The same junction was keyed `(a_id, b_id)` by one and
1169
+ * `(b_id, a_id)` by the other, and a push against a boot-built database rebuilt
1170
+ * the key. Now the side whose table sorts first, which is the order `db push`
1171
+ * already used wherever a file is named after its table.
1172
+ */
1173
+ function canonicalJunctionSide(table, relation, source, spec) {
1174
+ if (!spec || spec.declaringSides.length < 2) return {
1175
+ relation,
1176
+ source
1177
+ };
1178
+ const chosen = [...spec.declaringSides].sort((a, b) => {
1179
+ const left = bareTableName(getTableName(a.collection));
1180
+ const right = bareTableName(getTableName(b.collection));
1181
+ if (left !== right) return left < right ? -1 : 1;
1182
+ return a.junctionColumn < b.junctionColumn ? -1 : a.junctionColumn > b.junctionColumn ? 1 : 0;
1183
+ })[0].collection;
1184
+ if (chosen === source) return {
1185
+ relation,
1186
+ source
1187
+ };
1188
+ const resolved = Object.values(resolveCollectionRelations(chosen)).find((r) => isManyToMany(r) && bareTableName(r.through.table) === table);
1189
+ return resolved ? {
1190
+ relation: resolved,
1191
+ source: chosen
1192
+ } : {
1193
+ relation,
1194
+ source
1195
+ };
1196
+ }
1197
+ function planJunctionTable(tableName, reachedRelation, reachedSource, junctionSpecs, resolveCollection, triggerFunctionNeeded) {
1198
+ const { relation, source } = canonicalJunctionSide(bareTableName(tableName), reachedRelation, reachedSource, junctionSpecs.get(bareTableName(tableName)));
1466
1199
  if (!isManyToMany(relation)) throw new Error(`Internal: junction table "${tableName}" was reached from a ${relation.kind} relation.`);
1467
1200
  const table = bareTableName(tableName);
1468
1201
  const schema = "public";
@@ -1475,10 +1208,11 @@ function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCo
1475
1208
  const legacy = legacyForeignKeyName(slug);
1476
1209
  return column === generateForeignKeyName(slug) && legacy !== column ? legacy : void 0;
1477
1210
  };
1211
+ const via = linkLabel("relation", source, relation.relationName);
1478
1212
  const endpoint = (collection, column) => ({
1479
1213
  key: column,
1480
1214
  column,
1481
- type: primaryKeyPgType(collection),
1215
+ type: primaryKeyPgType(collection, via),
1482
1216
  nullable: false,
1483
1217
  primaryKey: false,
1484
1218
  unique: false,
@@ -1489,7 +1223,7 @@ function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCo
1489
1223
  column,
1490
1224
  targetSchema: schemaOf(collection),
1491
1225
  targetTable: bareTableName(getTableName(collection)),
1492
- targetColumn: getPrimaryKeyColumn(collection),
1226
+ targetColumn: getPrimaryKeyColumn(collection, via),
1493
1227
  onDelete
1494
1228
  }),
1495
1229
  source: { kind: "junction-key" }
@@ -1513,7 +1247,7 @@ function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCo
1513
1247
  columns.push(plan);
1514
1248
  }
1515
1249
  }
1516
- const policies = spec ? getJunctionSecurityRules(spec).flatMap((rule, index) => compileSecurityRule(getJunctionCollectionConfig(spec), rule, resolveCollection, false, rule.name ?? `#${index}`)) : [];
1250
+ const policies = spec ? withoutLiveSupersededNames(getJunctionSecurityRules(spec).flatMap((rule, index) => compileSecurityRule(getJunctionCollectionConfig(spec), rule, resolveCollection, false, rule.name ?? `#${index}`))) : [];
1517
1251
  return {
1518
1252
  schema,
1519
1253
  table,
@@ -1523,7 +1257,7 @@ function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCo
1523
1257
  declaringSlugs: spec?.declaringSides.map((side) => side.collection.slug) ?? [],
1524
1258
  columns,
1525
1259
  primaryKey: [sourceColumn, targetColumn],
1526
- indexes: [],
1260
+ indexes: planForeignKeyIndexes(schema, table, columns, [sourceColumn, targetColumn], [], (column) => column.source.kind === "junction-key"),
1527
1261
  vectorColumns: [],
1528
1262
  policies,
1529
1263
  triggers,
@@ -1538,12 +1272,29 @@ function planJunctionTable(tableName, relation, source, junctionSpecs, resolveCo
1538
1272
  */
1539
1273
  function planRelations(collections, tableEntries) {
1540
1274
  const plans = [];
1275
+ const fkOnTargetBySlug = /* @__PURE__ */ new Map();
1276
+ for (const other of collections) for (const otherRel of Object.values(resolveCollectionRelations(other))) {
1277
+ if (!hasForeignKeyOnTarget(otherRel)) continue;
1278
+ let otherTarget;
1279
+ try {
1280
+ otherTarget = otherRel.target();
1281
+ } catch {
1282
+ continue;
1283
+ }
1284
+ const list = fkOnTargetBySlug.get(otherTarget.slug) ?? [];
1285
+ list.push({
1286
+ other,
1287
+ otherRel
1288
+ });
1289
+ fkOnTargetBySlug.set(otherTarget.slug, list);
1290
+ }
1541
1291
  for (const [tableName, entry] of tableEntries) {
1542
1292
  const tableVar = getTableVarName(tableName);
1543
1293
  if (entry.junction) {
1544
1294
  const { relation, source } = entry.junction;
1545
1295
  if (!isManyToMany(relation)) continue;
1546
1296
  const target = relation.target();
1297
+ const via = linkLabel("relation", source, relation.relationName);
1547
1298
  const owningRelationName = sharedRelationName(relation, source);
1548
1299
  let inverse;
1549
1300
  try {
@@ -1557,7 +1308,7 @@ function planRelations(collections, tableEntries) {
1557
1308
  targetVar: getTableVarName(getTableName(source)),
1558
1309
  relationName: owningRelationName,
1559
1310
  fields: [relation.through.sourceColumn],
1560
- references: [getPrimaryKeyName(source)]
1311
+ references: [getPrimaryKeyName(source, via)]
1561
1312
  });
1562
1313
  if (inverseRelationName === owningRelationName) continue;
1563
1314
  plans.push({
@@ -1567,7 +1318,7 @@ function planRelations(collections, tableEntries) {
1567
1318
  targetVar: getTableVarName(getTableName(target)),
1568
1319
  relationName: inverseRelationName,
1569
1320
  fields: [relation.through.targetColumn],
1570
- references: [getPrimaryKeyName(target)]
1321
+ references: [getPrimaryKeyName(target, via)]
1571
1322
  });
1572
1323
  continue;
1573
1324
  }
@@ -1594,7 +1345,7 @@ function planRelations(collections, tableEntries) {
1594
1345
  targetVar,
1595
1346
  relationName,
1596
1347
  fields: [fieldKeyForColumn(collection, relation.localKey)],
1597
- references: [getPrimaryKeyName(target)]
1348
+ references: [getPrimaryKeyName(target, linkLabel("relation", collection, key))]
1598
1349
  });
1599
1350
  break;
1600
1351
  case "hasOne":
@@ -1602,7 +1353,11 @@ function planRelations(collections, tableEntries) {
1602
1353
  tableVar,
1603
1354
  key,
1604
1355
  kind: "one",
1605
- targetVar
1356
+ targetVar,
1357
+ relationName,
1358
+ fields: [relation.sourceKey ? fieldKeyForColumn(collection, relation.sourceKey) : getPrimaryKeyName(collection, linkLabel("relation", collection, key))],
1359
+ references: [fieldKeyForColumn(target, relation.foreignKeyOnTarget)],
1360
+ nullable: true
1606
1361
  });
1607
1362
  break;
1608
1363
  case "hasMany":
@@ -1626,38 +1381,28 @@ function planRelations(collections, tableEntries) {
1626
1381
  case "via": break;
1627
1382
  }
1628
1383
  }
1629
- for (const other of collections) {
1384
+ for (const { other, otherRel } of fkOnTargetBySlug.get(collection.slug) ?? []) {
1630
1385
  if (other.slug === collection.slug) continue;
1631
- for (const otherRel of Object.values(resolveCollectionRelations(other))) {
1632
- if (!hasForeignKeyOnTarget(otherRel)) continue;
1633
- let otherTarget;
1634
- try {
1635
- otherTarget = otherRel.target();
1636
- } catch {
1637
- continue;
1638
- }
1639
- if (otherTarget.slug !== collection.slug) continue;
1640
- const relationName = sharedRelationName(otherRel, other);
1641
- const dedupe = `${relationName}::belongsTo`;
1642
- if (emitted.has(dedupe)) continue;
1643
- emitted.add(dedupe);
1644
- const otherVar = getTableVarName(getTableName(other));
1645
- const fieldKey = fieldKeyForColumn(collection, otherRel.foreignKeyOnTarget);
1646
- plans.push({
1647
- tableVar,
1648
- key: `_synth_${otherVar}_${fieldKey}`,
1649
- kind: "one",
1650
- targetVar: otherVar,
1651
- relationName,
1652
- fields: [fieldKey],
1653
- references: [otherRel.sourceKey ? fieldKeyForColumn(other, otherRel.sourceKey) : getPrimaryKeyName(other)]
1654
- });
1655
- }
1386
+ const relationName = sharedRelationName(otherRel, other);
1387
+ const dedupe = `${relationName}::belongsTo`;
1388
+ if (emitted.has(dedupe)) continue;
1389
+ emitted.add(dedupe);
1390
+ const otherVar = getTableVarName(getTableName(other));
1391
+ const fieldKey = fieldKeyForColumn(collection, otherRel.foreignKeyOnTarget);
1392
+ plans.push({
1393
+ tableVar,
1394
+ key: `_synth_${otherVar}_${fieldKey}`,
1395
+ kind: "one",
1396
+ targetVar: otherVar,
1397
+ relationName,
1398
+ fields: [fieldKey],
1399
+ references: [otherRel.sourceKey ? fieldKeyForColumn(other, otherRel.sourceKey) : getPrimaryKeyName(other, linkLabel("relation", other, otherRel.relationName))]
1400
+ });
1656
1401
  }
1657
1402
  }
1658
1403
  return plans;
1659
1404
  }
1660
1405
  //#endregion
1661
- 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 };
1406
+ export { triggerStatements as a, SET_UPDATED_AT_FN as i, planSchema as n, sharedRelationName as o, quoteSqlLiteral as r, columnPgType as t };
1662
1407
 
1663
- //# sourceMappingURL=plan-schema-C0fxM8dY.js.map
1408
+ //# sourceMappingURL=plan-schema-B1YF_tYq.js.map