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

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 (208) hide show
  1. package/README.md +16 -0
  2. package/dist/{BranchService-DGPL6G_C.js → BranchService-UgKa416x.js} +47 -19
  3. package/dist/BranchService-UgKa416x.js.map +1 -0
  4. package/dist/PostgresBackendDriver.d.ts +187 -13
  5. package/dist/PostgresBootstrapper.d.ts +101 -2
  6. package/dist/auth/ensure-tables.d.ts +12 -0
  7. package/dist/auth/services.d.ts +78 -14
  8. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  9. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  10. package/dist/backup/backup-cli.d.ts +75 -2
  11. package/dist/backup/backup-service.d.ts +2 -2
  12. package/dist/backup/pg-binary.d.ts +5 -0
  13. package/dist/{backup-cli-24v4OlSp.js → backup-cli-yiKd1GsC.js} +168 -75
  14. package/dist/backup-cli-yiKd1GsC.js.map +1 -0
  15. package/dist/{backup-service-HQ9GC4tN.js → backup-service-AZWc2wvI.js} +104 -35
  16. package/dist/backup-service-AZWc2wvI.js.map +1 -0
  17. package/dist/boot-ddl-B9ZmBJ75.js +105 -0
  18. package/dist/boot-ddl-B9ZmBJ75.js.map +1 -0
  19. package/dist/cli-console-DGee3Zl7.js +225 -0
  20. package/dist/cli-console-DGee3Zl7.js.map +1 -0
  21. package/dist/cli-console.d.ts +39 -0
  22. package/dist/cli-errors-oVUuD8k0.js +2180 -0
  23. package/dist/cli-errors-oVUuD8k0.js.map +1 -0
  24. package/dist/cli-errors.d.ts +133 -9
  25. package/dist/{cli-flags-BglvpjHv.js → cli-flags-DnPHXjRH.js} +56 -6
  26. package/dist/cli-flags-DnPHXjRH.js.map +1 -0
  27. package/dist/cli-flags.d.ts +15 -0
  28. package/dist/cli-helpers.d.ts +88 -34
  29. package/dist/cli-output.d.ts +16 -28
  30. package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
  31. package/dist/cli-prompt-egtNKkZl.js.map +1 -0
  32. package/dist/cli-prompt.d.ts +19 -0
  33. package/dist/cli-push.d.ts +87 -0
  34. package/dist/cli-scratch-database.d.ts +33 -8
  35. package/dist/cli.js +826 -230
  36. package/dist/cli.js.map +1 -1
  37. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-D_V4oaZh.js} +43 -27
  38. package/dist/column-plan-helpers-D_V4oaZh.js.map +1 -0
  39. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C938nR3E.js} +25 -113
  40. package/dist/connection-C938nR3E.js.map +1 -0
  41. package/dist/connection.d.ts +13 -2
  42. package/dist/data-transformer.d.ts +0 -8
  43. package/dist/databasePoolManager-DZgnBOgj.js +119 -0
  44. package/dist/databasePoolManager-DZgnBOgj.js.map +1 -0
  45. package/dist/deferred-index-build-BqmEHGaD.js +1051 -0
  46. package/dist/deferred-index-build-BqmEHGaD.js.map +1 -0
  47. package/dist/{doctor-Cb2thZ8s.js → doctor-CUAAOFZm.js} +281 -89
  48. package/dist/doctor-CUAAOFZm.js.map +1 -0
  49. package/dist/ensure-collection-policies-Bb51V2OV.js +541 -0
  50. package/dist/ensure-collection-policies-Bb51V2OV.js.map +1 -0
  51. package/dist/ensure-collection-tables-egjGBjhv.js +532 -0
  52. package/dist/ensure-collection-tables-egjGBjhv.js.map +1 -0
  53. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-DMPWhJ6R.js} +180 -77
  54. package/dist/ensure-tables-DMPWhJ6R.js.map +1 -0
  55. package/dist/enum-labels-kept-Msq5gMcb.js +56 -0
  56. package/dist/enum-labels-kept-Msq5gMcb.js.map +1 -0
  57. package/dist/generate-drizzle-schema-DrA-YgXv.js +57 -0
  58. package/dist/generate-drizzle-schema-DrA-YgXv.js.map +1 -0
  59. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +29 -11
  60. package/dist/generate-drizzle-schema-logic-BaT0H7Gq.js.map +1 -0
  61. package/dist/generate-files-OoRJLEdk.js +199 -0
  62. package/dist/generate-files-OoRJLEdk.js.map +1 -0
  63. package/dist/generate-postgres-ddl-logic-B4k9djAV.js +152 -0
  64. package/dist/generate-postgres-ddl-logic-B4k9djAV.js.map +1 -0
  65. package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-BWN_PsMi.js} +2 -2
  66. package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-BWN_PsMi.js.map} +1 -1
  67. package/dist/generated-sql.d.ts +34 -0
  68. package/dist/history/HistoryService.d.ts +64 -5
  69. package/dist/history/ensure-history-table.d.ts +63 -4
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.es.js +139 -16738
  72. package/dist/index.es.js.map +1 -1
  73. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-BCc1Jym2.js} +654 -353
  74. package/dist/introspect-db-logic-BCc1Jym2.js.map +1 -0
  75. package/dist/introspect-db-queries-BhhJg0b8.js +364 -0
  76. package/dist/introspect-db-queries-BhhJg0b8.js.map +1 -0
  77. package/dist/open-rebase-CAjWpPjs.js +20895 -0
  78. package/dist/open-rebase-CAjWpPjs.js.map +1 -0
  79. package/dist/open-rebase.d.ts +94 -0
  80. package/dist/pg-error-utils-Bs_Ue4wn.js +395 -0
  81. package/dist/pg-error-utils-Bs_Ue4wn.js.map +1 -0
  82. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-CsNgK0qi.js} +279 -534
  83. package/dist/plan-schema-CsNgK0qi.js.map +1 -0
  84. package/dist/{policy-drift-DljYdrpW.js → policy-drift-BKC50y2p.js} +33 -9
  85. package/dist/policy-drift-BKC50y2p.js.map +1 -0
  86. package/dist/push-index-prebuild-C_qhyoJa.js +45 -0
  87. package/dist/push-index-prebuild-C_qhyoJa.js.map +1 -0
  88. package/dist/push-legacy-renames-5g9h3HOa.js +34 -0
  89. package/dist/push-legacy-renames-5g9h3HOa.js.map +1 -0
  90. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CrfEe__B.js} +24 -157
  91. package/dist/render-ddl-CrfEe__B.js.map +1 -0
  92. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-Bd-hJk08.js} +125 -1101
  93. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
  94. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-D7P4-ZYX.js} +138 -18
  95. package/dist/rls-enforcement-D7P4-ZYX.js.map +1 -0
  96. package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
  97. package/dist/schema/atlas-exclude.d.ts +42 -0
  98. package/dist/schema/auth-schema.d.ts +172 -2
  99. package/dist/schema/boot-ddl.d.ts +47 -0
  100. package/dist/schema/carved-out-migration.d.ts +5 -4
  101. package/dist/schema/classify-change.d.ts +29 -1
  102. package/dist/schema/collection-index.d.ts +45 -0
  103. package/dist/schema/column-plan-helpers.d.ts +38 -21
  104. package/dist/schema/deferred-index-build.d.ts +131 -0
  105. package/dist/schema/destructive-sql.d.ts +50 -2
  106. package/dist/schema/doctor-cli.js +110 -6
  107. package/dist/schema/doctor-cli.js.map +1 -1
  108. package/dist/schema/doctor-search-scale.d.ts +45 -0
  109. package/dist/schema/doctor.d.ts +53 -1
  110. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  111. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  112. package/dist/schema/enum-labels-kept.d.ts +26 -0
  113. package/dist/schema/generate-drizzle-schema.js +1 -1
  114. package/dist/schema/generate-files.d.ts +44 -0
  115. package/dist/schema/generate-next-step.d.ts +0 -13
  116. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  117. package/dist/schema/generate-postgres-ddl.js +10 -81
  118. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  119. package/dist/schema/generate-schema-commit.d.ts +12 -0
  120. package/dist/schema/generated-schema-diff.d.ts +7 -0
  121. package/dist/schema/introspect-db-logic.d.ts +63 -0
  122. package/dist/schema/introspect-db-queries.d.ts +21 -2
  123. package/dist/schema/introspect-db-search.d.ts +22 -0
  124. package/dist/schema/introspect-db-storage.d.ts +83 -0
  125. package/dist/schema/introspect-db-structure.d.ts +10 -0
  126. package/dist/schema/introspect-db.js +29 -21
  127. package/dist/schema/introspect-db.js.map +1 -1
  128. package/dist/schema/migrate-baseline.d.ts +33 -0
  129. package/dist/schema/migration-post-process.d.ts +34 -0
  130. package/dist/schema/plan/diff-plan.d.ts +127 -1
  131. package/dist/schema/plan/plan-schema.d.ts +16 -8
  132. package/dist/schema/plan/render-ddl.d.ts +8 -1
  133. package/dist/schema/plan/types.d.ts +45 -11
  134. package/dist/schema/push-index-prebuild.d.ts +25 -0
  135. package/dist/schema/push-legacy-renames.d.ts +20 -0
  136. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  137. package/dist/schema/undeclarable-constraints.d.ts +59 -0
  138. package/dist/search-column-BM-GV6vH.js +442 -0
  139. package/dist/search-column-BM-GV6vH.js.map +1 -0
  140. package/dist/security/policy-drift.d.ts +1 -1
  141. package/dist/security/rls-enforcement.d.ts +39 -4
  142. package/dist/services/BranchService.d.ts +29 -1
  143. package/dist/services/FetchService.d.ts +63 -12
  144. package/dist/services/PersistService.d.ts +15 -0
  145. package/dist/services/RelationService.d.ts +106 -3
  146. package/dist/services/cdc/CdcListener.d.ts +26 -10
  147. package/dist/services/cdc/identity-columns.d.ts +19 -0
  148. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  149. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  150. package/dist/services/channel-bus/index.d.ts +1 -1
  151. package/dist/services/channel-history.d.ts +32 -5
  152. package/dist/services/collection-helpers.d.ts +26 -1
  153. package/dist/services/field-viewer.d.ts +15 -1
  154. package/dist/services/pg-notify-listener.d.ts +95 -6
  155. package/dist/services/realtimeService.d.ts +467 -53
  156. package/dist/services/row-pipeline.d.ts +16 -9
  157. package/dist/services/socket-liveness.d.ts +85 -0
  158. package/dist/services/soft-delete.d.ts +12 -0
  159. package/dist/services/sql-script.d.ts +275 -0
  160. package/dist/services/vector-iterative-scan.d.ts +40 -0
  161. package/dist/services/write-depth.d.ts +22 -0
  162. package/dist/services/write-transaction-scope.d.ts +15 -3
  163. package/dist/socket-opt-ins.d.ts +64 -0
  164. package/dist/source-DB6xJv3p.js +873 -0
  165. package/dist/source-DB6xJv3p.js.map +1 -0
  166. package/dist/tsx-loader.d.ts +14 -0
  167. package/dist/utils/connection-string.d.ts +31 -0
  168. package/dist/utils/drizzle-conditions.d.ts +63 -3
  169. package/dist/utils/error-cause.d.ts +7 -0
  170. package/dist/utils/pg-error-utils.d.ts +2 -4
  171. package/dist/utils/result-rows.d.ts +10 -0
  172. package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
  173. package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
  174. package/dist/websocket-DUiiwQd2.js +1632 -0
  175. package/dist/websocket-DUiiwQd2.js.map +1 -0
  176. package/dist/websocket.d.ts +63 -18
  177. package/package.json +7 -7
  178. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  179. package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
  180. package/dist/arg-Dni7MzLB.js.map +0 -1
  181. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  182. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  183. package/dist/cli-errors-B8qHg02P.js +0 -798
  184. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  185. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  186. package/dist/cli-output-CNdMql-L.js +0 -47
  187. package/dist/cli-output-CNdMql-L.js.map +0 -1
  188. package/dist/collection-index-DdnsxiJ_.js +0 -286
  189. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  190. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  191. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  192. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  193. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  194. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  195. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  196. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  197. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  198. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  199. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  200. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  201. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  202. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  203. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  204. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  205. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  206. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  207. package/dist/source-Br7L7GOI.js +0 -410
  208. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The foreign-key indexes `rebase db push` builds before Atlas plans.
3
+ *
4
+ * Rebase derives a btree for every `belongsTo` key and every junction's target
5
+ * column (15b00afcd). On a table that already exists — every table of a
6
+ * project upgraded across that release — the index is missing, and a push left
7
+ * it to Atlas, which builds it inside the push's one transaction: a plain
8
+ * `CREATE INDEX`, holding writes to the table for the whole build, about 0.4 s
9
+ * at a million rows and 6 s at ten million. Atlas's own concurrent mode would
10
+ * run the entire apply outside a transaction, which the push relies on.
11
+ *
12
+ * So the push builds those first, `CONCURRENTLY`, with the builder the boot
13
+ * uses for the same indexes (`deferred-index-build.ts`: one at a time, a lock
14
+ * budget for getting the table, an INVALID leftover dropped and rebuilt, a
15
+ * claim shared with booting instances). Atlas then finds them and plans
16
+ * nothing for them. One it could not build is still Atlas's, as before.
17
+ *
18
+ * Only an index whose columns are already there: a column this push adds is
19
+ * created by Atlas, and its index with it, on a column that holds no values.
20
+ */
21
+ import { type CollectionConfig } from "@rebasepro/types";
22
+ import { type DeferredIndexBuild } from "./plan/diff-plan.js";
23
+ import { type Queryable } from "./ensure-collection-tables.js";
24
+ /** The derived foreign-key indexes a live table lacks (or holds INVALID) whose columns it already has. */
25
+ export declare function missingForeignKeyIndexes(client: Queryable, collections: CollectionConfig[]): Promise<DeferredIndexBuild[]>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The relation columns an aged database still holds under the name Rebase
3
+ * derived before 0.13 (`categorie_id`, `people_id`), for `db push` to rename
4
+ * before Atlas plans.
5
+ *
6
+ * Boot renames them losslessly (`diff-plan.ts`, `renameLegacyColumn`). Push
7
+ * runs Atlas, which sees only "drop `categorie_id`, add `category_id` NOT
8
+ * NULL": it listed the links as destructive changes to approve, and with
9
+ * `--allow-destructive` dropped them. A pipeline that runs only `db push`
10
+ * could not upgrade an aged database at all. Renaming first — the same
11
+ * metadata-only statement boot runs — leaves Atlas nothing to plan.
12
+ */
13
+ import { type CollectionConfig } from "@rebasepro/types";
14
+ import { type LegacyForeignKey } from "./plan/diff-plan.js";
15
+ import { type Queryable } from "./ensure-collection-tables.js";
16
+ export interface LegacyRename extends LegacyForeignKey {
17
+ /** `ALTER TABLE … RENAME COLUMN …;` */
18
+ sql: string;
19
+ }
20
+ export declare function legacyForeignKeyRenames(client: Queryable, collections: CollectionConfig[]): Promise<LegacyRename[]>;
@@ -45,6 +45,21 @@
45
45
  * a parser for a problem that does not need one.
46
46
  */
47
47
  export declare const RLS_BOOTSTRAP_STATEMENTS: readonly string[];
48
+ /**
49
+ * Whether the `rebase` schema and every RLS helper function are exactly what
50
+ * {@link RLS_BOOTSTRAP_STATEMENTS} would make them — one catalogue read.
51
+ *
52
+ * The boot ran those statements unconditionally, on every boot and on every
53
+ * source: four `CREATE OR REPLACE FUNCTION`s in a transaction under an
54
+ * advisory lock. Replacing a function rewrites its `pg_proc` row even when
55
+ * nothing changed, and every cached plan that inlined it — every plan of every
56
+ * statement a policy applies to, on every connection — is invalidated by it.
57
+ * A function that differs in anything `CREATE OR REPLACE` sets (body,
58
+ * language, volatility, strictness, parallel safety, security, leakproofness,
59
+ * cost, `SET` options), or is missing, answers `false`, and the statements run
60
+ * as before.
61
+ */
62
+ export declare function rlsRuntimeIsCurrent(run: (sql: string) => Promise<Record<string, unknown>[]>): Promise<boolean>;
48
63
  /** The same statements as one script, for migration files and raw clients. */
49
64
  export declare const RLS_BOOTSTRAP_SQL: string;
50
65
  /**
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The constraints a collection cannot declare, and how `db push` keeps Atlas
3
+ * from dropping them.
4
+ *
5
+ * `db push` is declarative: Atlas makes each managed table match `schema.sql`,
6
+ * and anything on the table that `schema.sql` does not mention is planned as a
7
+ * drop. The collection model declares a table's primary key, its columns'
8
+ * `NOT NULL`, `UNIQUE` and foreign keys, and nothing else. So a hand-written
9
+ * `CHECK (total >= 0)` — or one an adopted database arrived with, which
10
+ * `schema introspect` reads as `validation.min: 0` — was dropped by the first
11
+ * push, in the same `ALTER TABLE` as whatever else the push changed, with no
12
+ * prompt. From then on the database accepted every row the constraint used to
13
+ * refuse, while the form still enforced the rule and so hid that it had gone.
14
+ *
15
+ * EXCLUDE constraints are the same case: Atlas inspects them and plans their
16
+ * drop the same way. Triggers and functions are not: Atlas's free tier does
17
+ * not inspect either, so it never plans a drop for one. Measured on the pinned
18
+ * atlas 1.2.3 against PG 18; `introspect-check-constraints-e2e` pins all four.
19
+ *
20
+ * Ownership, as with indexes (`getForeignIndexExcludes` in `cli-helpers.ts`)
21
+ * and policies: a constraint the desired state declares is
22
+ * Atlas's to manage, and every other CHECK or EXCLUDE is somebody else's and is
23
+ * kept out of the plan with `--exclude`. Today the desired state declares
24
+ * neither kind, so every one is kept. A rule nobody wants any more is dropped
25
+ * by hand (`ALTER TABLE … DROP CONSTRAINT …`), which is the only statement that
26
+ * can say so.
27
+ *
28
+ * Side-effect free, so it is unit-tested without Atlas or a database.
29
+ */
30
+ /** A constraint kind the collection model has no way to declare. */
31
+ export type UndeclarableConstraintKind = "check" | "exclude";
32
+ /** A CHECK or EXCLUDE constraint, as the database holds it now. */
33
+ export interface ExistingConstraint {
34
+ schema: string;
35
+ table: string;
36
+ name: string;
37
+ kind: UndeclarableConstraintKind;
38
+ }
39
+ /**
40
+ * Every CHECK and EXCLUDE constraint on an ordinary or partitioned table
41
+ * outside the system schemas.
42
+ *
43
+ * `contype` `c` and `x` only. A primary key (`p`), `UNIQUE` (`u`), foreign key
44
+ * (`f`) and, from PG 18, a catalogued `NOT NULL` (`n`) are what a collection
45
+ * declares, so Atlas keeps managing them.
46
+ */
47
+ export declare const UNDECLARABLE_CONSTRAINTS_SQL = "\n SELECT n.nspname AS \"schema\",\n c.relname AS \"table\",\n con.conname AS \"name\",\n CASE con.contype WHEN 'c' THEN 'check' ELSE 'exclude' END AS \"kind\"\n FROM pg_constraint con\n JOIN pg_class c ON c.oid = con.conrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE con.contype IN ('c', 'x')\n AND c.relkind IN ('r', 'p')\n AND n.nspname NOT IN ('pg_catalog', 'information_schema')\n AND n.nspname NOT LIKE 'pg\\_%'\n";
48
+ /**
49
+ * The CHECK and EXCLUDE constraint names a desired state (`schema.sql`)
50
+ * declares — Atlas's to manage, and so never excluded.
51
+ */
52
+ export declare function declaredConstraintNames(desiredSql: string): Set<string>;
53
+ /**
54
+ * The `--exclude` patterns that keep Atlas from dropping the CHECK and EXCLUDE
55
+ * constraints the desired state does not declare. Three-part
56
+ * `schema.table.constraint`, which is what Atlas matches a constraint by: a
57
+ * two-part pattern reads as a table, matches nothing, and reports no error.
58
+ */
59
+ export declare function undeclaredConstraintExcludes(existing: readonly ExistingConstraint[], desiredSql: string): string[];
@@ -0,0 +1,442 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_MODE, DEFAULT_SEARCH_WEIGHT, isPostgresCollectionConfig } from "@rebasepro/types";
4
+ import { getTableName } from "@rebasepro/common";
5
+ import { toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
6
+ import { createHash } from "node:crypto";
7
+ //#region src/schema/search-column.ts
8
+ /**
9
+ * The one place a collection's `search` block becomes SQL.
10
+ *
11
+ * Four things describe a Postgres table in this codebase — the DDL generator,
12
+ * the Drizzle schema generator, the runtime table builder for BaaS mode, and
13
+ * the boot-time schema ensure — and each of them has, at some point, described
14
+ * a column differently from the others. The `varchar(255)` note in
15
+ * `generate-postgres-ddl-logic` is one such scar: the same property produced a
16
+ * capped column down one path and an uncapped one down the other, and nothing
17
+ * failed until a user hit the cap.
18
+ *
19
+ * So the search column is not implemented four times. It is computed once,
20
+ * here, and every generator renders the same {@link SearchColumnSpec}. There is
21
+ * a test asserting exactly that (`search-column-contract.test.ts`); the point of
22
+ * this module is that the test has something to assert *about*.
23
+ *
24
+ * ## Why the expressions look the way they do
25
+ *
26
+ * A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
27
+ * Postgres is stricter here than intuition. Verified against PostgreSQL 18:
28
+ *
29
+ * | expression | immutable |
30
+ * |-----------------------------------------|-----------|
31
+ * | `to_tsvector('spanish', col)` | yes |
32
+ * | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
33
+ * | `array_to_string(col, ' ')` | **no** |
34
+ * | `col::text` on `text[]` | **no** |
35
+ * | `to_jsonb(col)` | **no** |
36
+ * | `unaccent(col)` | **no** — dictionary lookup is STABLE |
37
+ * | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
38
+ * | `setweight(...) || setweight(...)` | yes |
39
+ *
40
+ * Three of the four things a real search column needs are therefore unavailable
41
+ * directly, which is why {@link searchHelperFunctions} exists: each wraps a
42
+ * stable built-in in an SQL function declared IMMUTABLE. That declaration is a
43
+ * promise, and it is a true one for these three — array joining, JSON string
44
+ * extraction and accent folding are all deterministic for a given input; the
45
+ * built-ins are marked stable only because they must account for element types
46
+ * and dictionaries in general.
47
+ *
48
+ * The alternative was to skip `unaccent` and text arrays entirely. That is not
49
+ * a real option in an accented language: Postgres stems `auditoría` to
50
+ * `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
51
+ * typed without accents misses every row that carries them.
52
+ */
53
+ /** Schema-qualified so a collection outside `public` still resolves them. */
54
+ var HELPER_SCHEMA = "public";
55
+ /**
56
+ * Names of the helper functions. Frozen: they are recorded in the stored
57
+ * generation expression of every search column ever created, so renaming one
58
+ * orphans every table that already has a search column.
59
+ */
60
+ var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
61
+ var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
62
+ /** Raised when a `search` block names something that cannot be searched. */
63
+ var SearchConfigError = class extends Error {
64
+ constructor(message) {
65
+ super(message);
66
+ this.name = "SearchConfigError";
67
+ }
68
+ };
69
+ /** The `search` block of a collection, or undefined when it has none. */
70
+ var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
71
+ /**
72
+ * Refuse a `search` block on a collection this engine does not store.
73
+ *
74
+ * The type only permits one on a `PostgresCollectionConfig`, so TypeScript
75
+ * already stops the ordinary case. This catches the rest — a JS config, a cast,
76
+ * a collection whose `engine` was changed after the block was written — because
77
+ * the alternative is the exact failure the block exists to prevent: a developer
78
+ * who declared what to index, saw no error, and got the substring fallback.
79
+ *
80
+ * Called with *every* collection, before the Postgres ones are filtered out.
81
+ */
82
+ var assertSearchIsPostgresOnly = (collections) => {
83
+ for (const collection of collections) {
84
+ if (isPostgresCollectionConfig(collection)) continue;
85
+ if (!collection.search) continue;
86
+ const engine = collection.engine ?? "non-postgres";
87
+ 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.`);
88
+ }
89
+ };
90
+ var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
91
+ /**
92
+ * Classify a property for search purposes.
93
+ *
94
+ * Deliberately narrower than the schema plan's `PgType`: search only cares
95
+ * whether a value reaches text, and the mapping from property to *physical*
96
+ * type is asserted against the plan in the contract test rather than duplicated
97
+ * here.
98
+ *
99
+ * Returns null for anything that is not text-bearing, which the caller turns
100
+ * into a boot error naming the property.
101
+ */
102
+ var classify = (prop) => {
103
+ switch (prop.type) {
104
+ case "string": {
105
+ const sp = prop;
106
+ if (sp.enum) return {
107
+ kind: "text",
108
+ reason: "enum"
109
+ };
110
+ if (sp.isId === "uuid" || sp.columnType === "uuid") return {
111
+ kind: "text",
112
+ reason: "uuid"
113
+ };
114
+ return { kind: "text" };
115
+ }
116
+ case "map":
117
+ if (prop.columnType === "json") return {
118
+ kind: "jsonb",
119
+ reason: "json"
120
+ };
121
+ return { kind: "jsonb" };
122
+ case "array": {
123
+ const ap = prop;
124
+ let colType = ap.columnType;
125
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
126
+ const of = ap.of;
127
+ if (of.type === "string") colType = "text[]";
128
+ else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
129
+ else if (of.type === "boolean") colType = "boolean[]";
130
+ }
131
+ if (colType === "text[]") return { kind: "text_array" };
132
+ if (colType === "json") return {
133
+ kind: "jsonb",
134
+ reason: "json"
135
+ };
136
+ if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
137
+ kind: "text_array",
138
+ reason: "non_text_array"
139
+ };
140
+ return { kind: "jsonb" };
141
+ }
142
+ default: return null;
143
+ }
144
+ };
145
+ var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
146
+ /** SQL reading one field as plain text, before normalization. */
147
+ var rawTextSql = (field) => {
148
+ const col = `"${field.column}"`;
149
+ if (field.kind === "text") return `coalesce(${col}, '')`;
150
+ if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
151
+ 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))`;
152
+ };
153
+ var quote = (v) => `'${v.replace(/'/g, "''")}'`;
154
+ /**
155
+ * Resolve and validate one declared field path.
156
+ *
157
+ * A path that does not resolve throws. The whole point of an explicit block is
158
+ * that the author knows what is indexed; a silently dropped field would make it
159
+ * a guess again, and the failure — a search that returns nothing for content
160
+ * that is plainly in the row — is invisible from the outside.
161
+ */
162
+ var resolveField = (entry, collection, cfg) => {
163
+ const path = typeof entry === "string" ? entry : entry.path;
164
+ const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? DEFAULT_SEARCH_WEIGHT;
165
+ const where = `${collection.slug}.search`;
166
+ if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
167
+ const [head, ...rest] = path.split(".");
168
+ const prop = collection.properties?.[head];
169
+ if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
170
+ const classified = classify(prop);
171
+ 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).`);
172
+ 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.`);
173
+ if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
174
+ 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.`);
175
+ if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
176
+ 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.`);
177
+ const column = columnNameOf(head, prop);
178
+ const raw = rawTextSql({
179
+ column,
180
+ jsonPath: rest,
181
+ kind: classified.kind
182
+ });
183
+ const textSql = normalize(raw, cfg.unaccent === true);
184
+ const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;
185
+ return {
186
+ path,
187
+ column,
188
+ jsonPath: rest,
189
+ kind: classified.kind,
190
+ weight,
191
+ sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
192
+ textSql,
193
+ foldedTextSql: normalize(raw, true)
194
+ };
195
+ };
196
+ /**
197
+ * Build the full spec for a collection, or undefined when it has not opted in.
198
+ *
199
+ * Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
200
+ * at boot surface that as a startup failure — a search block that half-works is
201
+ * worse than one that refuses.
202
+ */
203
+ var buildSearchColumnSpec = (collection) => {
204
+ const cfg = getSearchConfig(collection);
205
+ if (!cfg) return void 0;
206
+ 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.`);
207
+ const table = getTableName(collection);
208
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
209
+ const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;
210
+ 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.`);
211
+ const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
212
+ const seen = /* @__PURE__ */ new Set();
213
+ for (const f of fields) {
214
+ if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
215
+ seen.add(f.path);
216
+ }
217
+ const mode = cfg.mode ?? DEFAULT_SEARCH_MODE;
218
+ const extensions = [];
219
+ if (cfg.unaccent || mode === "hybrid") extensions.push("unaccent");
220
+ if (cfg.fuzzy) extensions.push("pg_trgm");
221
+ const spec = {
222
+ schema,
223
+ table,
224
+ column,
225
+ language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,
226
+ unaccent: cfg.unaccent === true,
227
+ mode,
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 || 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) $$;`);
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
+ export { visibleColumnProjection as _, buildSearchColumnSpec as a, searchColumnDefinition as c, searchColumnTypeSql as d, searchExtensionStatements as f, searchStampGuards as g, searchIndexStatements as h, assertSearchIsPostgresOnly as i, searchColumnNames as l, searchIndexNames as m, SEARCH_TEXT_FN as n, fuzzyColumnDefinition as o, searchHelperFunctions as p, SEARCH_UNACCENT_FN as r, hiddenColumnsOption as s, SEARCH_STAMP_PREFIX as t, searchColumnStamps as u };
441
+
442
+ //# sourceMappingURL=search-column-BM-GV6vH.js.map