@rebasepro/server-postgres 0.24.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 (162) hide show
  1. package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
  2. package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +83 -2
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +6 -0
  6. package/dist/backup/backup-cli.d.ts +14 -0
  7. package/dist/backup/backup-service.d.ts +2 -2
  8. package/dist/backup/pg-binary.d.ts +5 -0
  9. package/dist/{backup-cli-DW5p9_zv.js → backup-cli-aUfRv16h.js} +37 -29
  10. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  11. package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
  12. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  13. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  14. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  15. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  16. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  17. package/dist/cli-errors.d.ts +51 -5
  18. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  19. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  20. package/dist/cli-helpers.d.ts +40 -0
  21. package/dist/cli-output.d.ts +16 -28
  22. package/dist/cli-push.d.ts +87 -0
  23. package/dist/cli.js +563 -182
  24. package/dist/cli.js.map +1 -1
  25. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
  26. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
  27. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  28. package/dist/connection-BPoNILnD.js.map +1 -0
  29. package/dist/connection.d.ts +9 -1
  30. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  31. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  32. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
  33. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  34. package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
  35. package/dist/doctor-C4WiftHm.js.map +1 -0
  36. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  37. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  38. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  39. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  40. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CHsgyLqg.js} +172 -76
  41. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  42. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  43. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  44. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  45. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  46. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
  47. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  48. package/dist/generate-files-CdEckX6c.js +199 -0
  49. package/dist/generate-files-CdEckX6c.js.map +1 -0
  50. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
  51. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
  52. package/dist/generated-sql.d.ts +8 -2
  53. package/dist/history/HistoryService.d.ts +38 -6
  54. package/dist/history/ensure-history-table.d.ts +42 -4
  55. package/dist/index.es.js +2922 -7475
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
  58. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  59. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
  60. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  61. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  62. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  63. package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
  64. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  65. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
  66. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  67. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  68. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  69. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  70. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  71. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
  72. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  73. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
  74. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  75. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
  76. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  77. package/dist/schema/auth-schema.d.ts +2 -2
  78. package/dist/schema/boot-ddl.d.ts +47 -0
  79. package/dist/schema/collection-index.d.ts +45 -0
  80. package/dist/schema/column-plan-helpers.d.ts +5 -0
  81. package/dist/schema/deferred-index-build.d.ts +131 -0
  82. package/dist/schema/destructive-sql.d.ts +7 -0
  83. package/dist/schema/doctor-cli.js +110 -6
  84. package/dist/schema/doctor-cli.js.map +1 -1
  85. package/dist/schema/doctor-search-scale.d.ts +45 -0
  86. package/dist/schema/doctor.d.ts +20 -1
  87. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  88. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  89. package/dist/schema/enum-labels-kept.d.ts +26 -0
  90. package/dist/schema/generate-drizzle-schema.js +1 -1
  91. package/dist/schema/generate-files.d.ts +44 -0
  92. package/dist/schema/generate-next-step.d.ts +0 -13
  93. package/dist/schema/generate-postgres-ddl.js +10 -81
  94. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  95. package/dist/schema/introspect-db-logic.d.ts +12 -0
  96. package/dist/schema/introspect-db-queries.d.ts +11 -1
  97. package/dist/schema/introspect-db-structure.d.ts +10 -0
  98. package/dist/schema/introspect-db.js +4 -5
  99. package/dist/schema/introspect-db.js.map +1 -1
  100. package/dist/schema/migration-post-process.d.ts +34 -0
  101. package/dist/schema/plan/diff-plan.d.ts +126 -0
  102. package/dist/schema/plan/types.d.ts +8 -0
  103. package/dist/schema/push-index-prebuild.d.ts +25 -0
  104. package/dist/schema/push-legacy-renames.d.ts +20 -0
  105. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  106. package/dist/security/rls-enforcement.d.ts +32 -4
  107. package/dist/services/FetchService.d.ts +58 -8
  108. package/dist/services/PersistService.d.ts +15 -0
  109. package/dist/services/RelationService.d.ts +88 -3
  110. package/dist/services/cdc/CdcListener.d.ts +17 -6
  111. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  112. package/dist/services/channel-history.d.ts +15 -4
  113. package/dist/services/collection-helpers.d.ts +15 -0
  114. package/dist/services/field-viewer.d.ts +15 -1
  115. package/dist/services/pg-notify-listener.d.ts +7 -0
  116. package/dist/services/realtimeService.d.ts +314 -14
  117. package/dist/services/row-pipeline.d.ts +16 -9
  118. package/dist/services/socket-liveness.d.ts +42 -2
  119. package/dist/services/sql-script.d.ts +207 -3
  120. package/dist/services/vector-iterative-scan.d.ts +40 -0
  121. package/dist/services/write-depth.d.ts +6 -0
  122. package/dist/services/write-transaction-scope.d.ts +9 -0
  123. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  124. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  125. package/dist/socket-opt-ins.d.ts +64 -0
  126. package/dist/source-DBDbBTL4.js +811 -0
  127. package/dist/source-DBDbBTL4.js.map +1 -0
  128. package/dist/tsx-loader.d.ts +14 -0
  129. package/dist/utils/drizzle-conditions.d.ts +21 -1
  130. package/dist/utils/error-cause.d.ts +7 -0
  131. package/dist/utils/pg-error-utils.d.ts +2 -4
  132. package/dist/utils/result-rows.d.ts +10 -0
  133. package/dist/websocket.d.ts +13 -0
  134. package/package.json +7 -7
  135. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  136. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  137. package/dist/cli-errors-DsA-K9uP.js +0 -893
  138. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  139. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  140. package/dist/cli-output-CNdMql-L.js +0 -47
  141. package/dist/cli-output-CNdMql-L.js.map +0 -1
  142. package/dist/collection-index-DdnsxiJ_.js +0 -286
  143. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  144. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  145. package/dist/doctor-C-sYWbmt.js.map +0 -1
  146. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  147. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  148. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  149. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  150. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  151. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  152. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  153. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  154. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  155. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  156. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  157. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  158. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  159. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  160. package/dist/source-Br7L7GOI.js +0 -410
  161. package/dist/source-Br7L7GOI.js.map +0 -1
  162. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -0,0 +1,199 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
+ import { i as outError, n as describeFailure, r as out, t as chalk } from "./source-DBDbBTL4.js";
5
+ import { t as generateSchema } from "./generate-drizzle-schema-logic-B74WN37S.js";
6
+ import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "./generate-postgres-ddl-logic-B64O2oZF.js";
7
+ import { loadCollectionsFromDirectory } from "@rebasepro/server";
8
+ import { declaredDatabaseExtensions } from "@rebasepro/types";
9
+ import path from "path";
10
+ import * as fs$1 from "fs";
11
+ import { promises } from "fs";
12
+ import { pathToFileURL } from "url";
13
+ //#region src/schema/generate-next-step.ts
14
+ /**
15
+ * The line `rebase schema generate` ends on.
16
+ *
17
+ * Its own module because `generate-drizzle-schema.ts` uses `import.meta` to
18
+ * decide whether it was executed directly, which the driver's jest suite cannot
19
+ * load at all — so the one sentence in that file with a decision in it had no
20
+ * test and could not have one where it stood.
21
+ *
22
+ * Printed only by `rebase schema generate` itself. `db push` and `db generate`
23
+ * run the same generator as their first step, and inside them the line was
24
+ * wrong advice: `db push` was told to run `db generate`, and `db generate` to
25
+ * run itself.
26
+ */
27
+ /**
28
+ * A command name, highlighted the way the rest of the driver's output is: with
29
+ * chalk, which drops the colour under `NO_COLOR`, when stdout is not a terminal,
30
+ * and in CI logs. The escapes used to be written by hand here, so
31
+ * `NO_COLOR=1 rebase schema generate > log` put `\x1b[1m\x1b[44m…` in the file.
32
+ */
33
+ var command = (text) => chalk.bold.bgBlue.black(text);
34
+ /**
35
+ * What to do with the file that was just generated.
36
+ *
37
+ * Not the same answer on the managed development database: `rebase db generate`
38
+ * plans with Atlas, Atlas needs a second empty database to diff against, and
39
+ * PGlite serves exactly one — so the CLI refuses that command outright there.
40
+ * This line recommended it unconditionally, which meant every stock scaffold
41
+ * was told, by the tool itself, to run something the same tool would refuse.
42
+ *
43
+ * The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the
44
+ * ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is
45
+ * started to make it — and the driver cannot make it for itself: on the managed
46
+ * path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres
47
+ * on loopback. Absent (the driver run directly, outside the CLI) keeps the
48
+ * original wording.
49
+ */
50
+ var nextStepAfterGenerate = () => {
51
+ if (process.env.REBASE_DEV_DATABASE_KIND === "managed") return `Start or restart ${command("rebase dev")} — boot applies your collections to the managed development database.`;
52
+ return `You can now run ${command("rebase db generate")} to generate the SQL migration files.`;
53
+ };
54
+ //#endregion
55
+ //#region src/schema/generate-files.ts
56
+ var generate_files_exports = /* @__PURE__ */ __exportAll({
57
+ GenerationFailed: () => GenerationFailed,
58
+ writeDrizzleSchema: () => writeDrizzleSchema,
59
+ writePostgresDdl: () => writePostgresDdl
60
+ });
61
+ /**
62
+ * The two generators `rebase db push` runs as its step 1, as functions.
63
+ *
64
+ * They used to exist only as scripts, each its own `tsx` process: the driver
65
+ * (itself a `tsx` process) spawned one for the Drizzle schema and then one for
66
+ * the DDL, and each paid a Node start, tsx's loader and an import of the whole
67
+ * server package before reading a single collection — about 0.5 s apiece, a
68
+ * second per push. The driver process is one-shot, so a fresh module graph per
69
+ * generation bought nothing: the collections cannot change under it.
70
+ *
71
+ * The scripts remain (`generate-drizzle-schema.ts`, `generate-postgres-ddl.ts`)
72
+ * for `--watch`, which re-imports the collections on every save and does need a
73
+ * process of its own — Node's module cache would serve it the first version
74
+ * forever.
75
+ *
76
+ * Both functions print their own success lines and their own error, exactly as
77
+ * the scripts did, and throw {@link GenerationFailed} after printing — so a
78
+ * caller can tell "the generator said why" from "the generator never ran".
79
+ */
80
+ /** Thrown once the generator has printed why it failed. */
81
+ var GenerationFailed = class extends Error {
82
+ constructor(message, options) {
83
+ super(message, options);
84
+ this.name = "GenerationFailed";
85
+ }
86
+ };
87
+ /** The collections, in slug order so the output does not depend on the directory's. */
88
+ async function loadSortedCollections(collectionsPath) {
89
+ const loaded = await loadCollectionsFromDirectory(path.resolve(collectionsPath));
90
+ const collections = Array.isArray(loaded) ? [...loaded] : [];
91
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
92
+ return collections;
93
+ }
94
+ /** `src/schema.generated.ts`, from the collections. */
95
+ async function writeDrizzleSchema(options) {
96
+ try {
97
+ const schemaContent = generateSchema(await loadSortedCollections(options.collectionsPath));
98
+ if (options.outputPath) {
99
+ await promises.mkdir(path.dirname(options.outputPath), { recursive: true });
100
+ await promises.writeFile(options.outputPath, schemaContent);
101
+ out(`✅ Drizzle schema generated successfully at ${options.outputPath}`);
102
+ } else {
103
+ out("✅ Drizzle schema generated successfully.");
104
+ out(String(schemaContent));
105
+ }
106
+ if (options.nextStep !== false) out(nextStepAfterGenerate());
107
+ } catch (error) {
108
+ outError(`Error generating schema: ${describeFailure(error)}`);
109
+ throw new GenerationFailed("Drizzle schema generation failed", { cause: error });
110
+ }
111
+ }
112
+ /** Where a project declares its resources, relative to the config directory. */
113
+ var RESOURCE_ENTRIES = [
114
+ "resources.ts",
115
+ "resources.js",
116
+ "resources/index.ts",
117
+ "resources/index.js"
118
+ ];
119
+ /**
120
+ * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can
121
+ * answer.
122
+ *
123
+ * The DDL generator loads only the collections directory, so nothing else in it
124
+ * would ever import the resources module — and a permission that reads as
125
+ * absent because nobody loaded it is the worst shape this could take: the push
126
+ * succeeds, `vector.sql` quietly omits the `CREATE EXTENSION`, and the failure
127
+ * lands one statement later looking like a database problem.
128
+ *
129
+ * Imported directly rather than through the config index, for the reason
130
+ * `boot/resource-loading.ts` gives: re-exporting from the index would work only
131
+ * for projects that remember to.
132
+ *
133
+ * A project with no resources module is the normal case and declares nothing.
134
+ * An import that *throws* is not swallowed — a resources module that does not
135
+ * evaluate is a configuration error, and treating it as "declared nothing"
136
+ * would turn it into a silently missing extension.
137
+ */
138
+ async function loadDeclaredResources(configDir) {
139
+ const entry = RESOURCE_ENTRIES.map((name) => path.join(configDir, name)).find((candidate) => fs$1.existsSync(candidate));
140
+ if (!entry) return;
141
+ await import(pathToFileURL(entry).href);
142
+ }
143
+ /**
144
+ * `schema.sql` (Atlas's desired state) and, beside it, `policies.sql`,
145
+ * `search.sql`, `vector.sql` and `triggers.sql` — the parts Atlas does not own.
146
+ */
147
+ async function writePostgresDdl(options) {
148
+ try {
149
+ const resolvedPath = path.resolve(options.collectionsPath);
150
+ await loadDeclaredResources(path.dirname(resolvedPath));
151
+ const collections = await loadSortedCollections(resolvedPath);
152
+ const ddlContent = generatePostgresDdl(collections, {
153
+ includePolicies: false,
154
+ includeSearch: false,
155
+ includeVector: false
156
+ });
157
+ const policiesContent = generatePostgresPoliciesDdl(collections);
158
+ const searchContent = generatePostgresSearchDdl(collections);
159
+ const vectorContent = generatePostgresVectorDdl(collections, { extensions: declaredDatabaseExtensions() });
160
+ const triggersContent = generatePostgresTriggersDdl(collections);
161
+ const outputPath = options.outputPath;
162
+ if (outputPath) {
163
+ const outputDir = path.dirname(outputPath);
164
+ await promises.mkdir(outputDir, { recursive: true });
165
+ await promises.writeFile(outputPath, ddlContent);
166
+ out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);
167
+ const policiesPath = path.join(outputDir, "policies.sql");
168
+ await promises.writeFile(policiesPath, policiesContent);
169
+ out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);
170
+ const searchPath = path.join(outputDir, "search.sql");
171
+ if (searchContent) {
172
+ await promises.writeFile(searchPath, searchContent);
173
+ out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);
174
+ } else if (fs$1.existsSync(searchPath)) await promises.rm(searchPath);
175
+ const vectorPath = path.join(outputDir, "vector.sql");
176
+ if (vectorContent) {
177
+ await promises.writeFile(vectorPath, vectorContent);
178
+ out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);
179
+ } else if (fs$1.existsSync(vectorPath)) await promises.rm(vectorPath);
180
+ const triggersPath = path.join(outputDir, "triggers.sql");
181
+ if (triggersContent) {
182
+ await promises.writeFile(triggersPath, triggersContent);
183
+ out(`✅ PostgreSQL trigger DDL generated successfully at ${triggersPath}`);
184
+ } else if (fs$1.existsSync(triggersPath)) await promises.rm(triggersPath);
185
+ } else {
186
+ out("✅ PostgreSQL DDL generated successfully.");
187
+ out(String(ddlContent));
188
+ out("\n✅ PostgreSQL Policies DDL generated successfully.");
189
+ out(String(policiesContent));
190
+ }
191
+ } catch (error) {
192
+ outError(`Error generating DDL schema: ${describeFailure(error)}`);
193
+ throw new GenerationFailed("Postgres DDL generation failed", { cause: error });
194
+ }
195
+ }
196
+ //#endregion
197
+ export { writeDrizzleSchema as n, writePostgresDdl as r, generate_files_exports as t };
198
+
199
+ //# sourceMappingURL=generate-files-CdEckX6c.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-files-CdEckX6c.js","names":[],"sources":["../src/schema/generate-next-step.ts","../src/schema/generate-files.ts"],"sourcesContent":["/**\n * The line `rebase schema generate` ends on.\n *\n * Its own module because `generate-drizzle-schema.ts` uses `import.meta` to\n * decide whether it was executed directly, which the driver's jest suite cannot\n * load at all — so the one sentence in that file with a decision in it had no\n * test and could not have one where it stood.\n *\n * Printed only by `rebase schema generate` itself. `db push` and `db generate`\n * run the same generator as their first step, and inside them the line was\n * wrong advice: `db push` was told to run `db generate`, and `db generate` to\n * run itself.\n */\nimport chalk from \"chalk\";\n\n/**\n * A command name, highlighted the way the rest of the driver's output is: with\n * chalk, which drops the colour under `NO_COLOR`, when stdout is not a terminal,\n * and in CI logs. The escapes used to be written by hand here, so\n * `NO_COLOR=1 rebase schema generate > log` put `\\x1b[1m\\x1b[44m…` in the file.\n */\nconst command = (text: string): string => chalk.bold.bgBlue.black(text);\n\n/**\n * What to do with the file that was just generated.\n *\n * Not the same answer on the managed development database: `rebase db generate`\n * plans with Atlas, Atlas needs a second empty database to diff against, and\n * PGlite serves exactly one — so the CLI refuses that command outright there.\n * This line recommended it unconditionally, which meant every stock scaffold\n * was told, by the tool itself, to run something the same tool would refuse.\n *\n * The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the\n * ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is\n * started to make it — and the driver cannot make it for itself: on the managed\n * path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres\n * on loopback. Absent (the driver run directly, outside the CLI) keeps the\n * original wording.\n */\nexport const nextStepAfterGenerate = (): string => {\n if (process.env.REBASE_DEV_DATABASE_KIND === \"managed\") {\n return `Start or restart ${command(\"rebase dev\")} — boot applies your collections to the managed development database.`;\n }\n\n return `You can now run ${command(\"rebase db generate\")} to generate the SQL migration files.`;\n};\n","import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { CollectionConfig, declaredDatabaseExtensions } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport {\n generatePostgresDdl,\n generatePostgresPoliciesDdl,\n generatePostgresSearchDdl,\n generatePostgresTriggersDdl,\n generatePostgresVectorDdl\n} from \"./generate-postgres-ddl-logic\";\nimport { describeFailure, out, outError } from \"../cli-output\";\nimport { nextStepAfterGenerate } from \"./generate-next-step\";\n\n/**\n * The two generators `rebase db push` runs as its step 1, as functions.\n *\n * They used to exist only as scripts, each its own `tsx` process: the driver\n * (itself a `tsx` process) spawned one for the Drizzle schema and then one for\n * the DDL, and each paid a Node start, tsx's loader and an import of the whole\n * server package before reading a single collection — about 0.5 s apiece, a\n * second per push. The driver process is one-shot, so a fresh module graph per\n * generation bought nothing: the collections cannot change under it.\n *\n * The scripts remain (`generate-drizzle-schema.ts`, `generate-postgres-ddl.ts`)\n * for `--watch`, which re-imports the collections on every save and does need a\n * process of its own — Node's module cache would serve it the first version\n * forever.\n *\n * Both functions print their own success lines and their own error, exactly as\n * the scripts did, and throw {@link GenerationFailed} after printing — so a\n * caller can tell \"the generator said why\" from \"the generator never ran\".\n */\n\n/** Thrown once the generator has printed why it failed. */\nexport class GenerationFailed extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"GenerationFailed\";\n }\n}\n\nexport interface GenerateFileOptions {\n /** The collections directory (or single module), resolved against the cwd. */\n collectionsPath: string;\n /** Where to write. Absent: print to stdout instead. */\n outputPath?: string;\n /**\n * End on the line that says what to run next. Only `rebase schema\n * generate` itself wants it: inside `db push` it told the push to run\n * `db generate`, and inside `db generate` to run itself.\n */\n nextStep?: boolean;\n}\n\n/** The collections, in slug order so the output does not depend on the directory's. */\nasync function loadSortedCollections(collectionsPath: string): Promise<CollectionConfig[]> {\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n const loaded = await loadCollectionsFromDirectory(path.resolve(collectionsPath));\n // A copy: the loader's array is its caller's, and in-process the same\n // collections are read again by the steps after this one.\n const collections: CollectionConfig[] = Array.isArray(loaded) ? [...loaded] : [];\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n return collections;\n}\n\n/** `src/schema.generated.ts`, from the collections. */\nexport async function writeDrizzleSchema(options: GenerateFileOptions): Promise<void> {\n try {\n const collections = await loadSortedCollections(options.collectionsPath);\n const schemaContent = generateSchema(collections);\n\n if (options.outputPath) {\n await fsPromises.mkdir(path.dirname(options.outputPath), { recursive: true });\n await fsPromises.writeFile(options.outputPath, schemaContent);\n out(`✅ Drizzle schema generated successfully at ${options.outputPath}`);\n } else {\n out(\"✅ Drizzle schema generated successfully.\");\n out(String(schemaContent));\n }\n\n if (options.nextStep !== false) out(nextStepAfterGenerate());\n } catch (error) {\n outError(`Error generating schema: ${describeFailure(error)}`);\n // `rebase db push` runs this as step 1 and must stop on it: going on\n // applied whatever `src/schema.generated.ts` was already there.\n throw new GenerationFailed(\"Drizzle schema generation failed\", { cause: error });\n }\n}\n\n/** Where a project declares its resources, relative to the config directory. */\nconst RESOURCE_ENTRIES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/**\n * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can\n * answer.\n *\n * The DDL generator loads only the collections directory, so nothing else in it\n * would ever import the resources module — and a permission that reads as\n * absent because nobody loaded it is the worst shape this could take: the push\n * succeeds, `vector.sql` quietly omits the `CREATE EXTENSION`, and the failure\n * lands one statement later looking like a database problem.\n *\n * Imported directly rather than through the config index, for the reason\n * `boot/resource-loading.ts` gives: re-exporting from the index would work only\n * for projects that remember to.\n *\n * A project with no resources module is the normal case and declares nothing.\n * An import that *throws* is not swallowed — a resources module that does not\n * evaluate is a configuration error, and treating it as \"declared nothing\"\n * would turn it into a silently missing extension.\n */\nasync function loadDeclaredResources(configDir: string): Promise<void> {\n const entry = RESOURCE_ENTRIES\n .map(name => path.join(configDir, name))\n .find(candidate => fs.existsSync(candidate));\n if (!entry) return;\n await import(pathToFileURL(entry).href);\n}\n\n/**\n * `schema.sql` (Atlas's desired state) and, beside it, `policies.sql`,\n * `search.sql`, `vector.sql` and `triggers.sql` — the parts Atlas does not own.\n */\nexport async function writePostgresDdl(options: GenerateFileOptions): Promise<void> {\n try {\n const resolvedPath = path.resolve(options.collectionsPath);\n\n // `<config>/collections` is the path this is given, so the config\n // directory is its parent — the same relationship `rebase resources`\n // and the boot loader assume.\n await loadDeclaredResources(path.dirname(resolvedPath));\n\n const collections = await loadSortedCollections(resolvedPath);\n\n // `schema.sql` is Atlas's desired state, and it carries neither the RLS\n // policies, nor the search apparatus, nor the vector columns: Atlas\n // manages none of them, and in the search and vector cases cannot. All\n // three are written beside it and applied by the CLI in their own right.\n const ddlContent = generatePostgresDdl(collections, {\n includePolicies: false,\n includeSearch: false,\n includeVector: false\n });\n const policiesContent = generatePostgresPoliciesDdl(collections);\n const searchContent = generatePostgresSearchDdl(collections);\n const vectorContent = generatePostgresVectorDdl(collections, {\n extensions: declaredDatabaseExtensions()\n });\n const triggersContent = generatePostgresTriggersDdl(collections);\n\n const outputPath = options.outputPath;\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, ddlContent);\n out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);\n\n const policiesPath = path.join(outputDir, \"policies.sql\");\n await fsPromises.writeFile(policiesPath, policiesContent);\n out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);\n\n // Removed when the last `search` block goes: the CLI applies this\n // file whenever it exists, and a stale one would keep re-creating\n // functions for collections that no longer want them.\n const searchPath = path.join(outputDir, \"search.sql\");\n if (searchContent) {\n await fsPromises.writeFile(searchPath, searchContent);\n out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);\n } else if (fs.existsSync(searchPath)) {\n await fsPromises.rm(searchPath);\n }\n\n // Removed when the last `vector` property goes, for the same reason\n // `search.sql` is: the CLI applies whichever of these files exists,\n // and a stale one would keep re-adding a column the collections no\n // longer declare — which Atlas, no longer told to exclude it, would\n // then plan a DROP for on the very next push.\n const vectorPath = path.join(outputDir, \"vector.sql\");\n if (vectorContent) {\n await fsPromises.writeFile(vectorPath, vectorContent);\n out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);\n } else if (fs.existsSync(vectorPath)) {\n await fsPromises.rm(vectorPath);\n }\n\n // `autoValue: \"on_update\"`, on the same terms as the two above: a\n // trigger is a function plus a binding, which Atlas's free tier\n // will not parse, and a stale file would keep re-installing a\n // trigger for a column nothing declares any more.\n const triggersPath = path.join(outputDir, \"triggers.sql\");\n if (triggersContent) {\n await fsPromises.writeFile(triggersPath, triggersContent);\n out(`✅ PostgreSQL trigger DDL generated successfully at ${triggersPath}`);\n } else if (fs.existsSync(triggersPath)) {\n await fsPromises.rm(triggersPath);\n }\n } else {\n out(\"✅ PostgreSQL DDL generated successfully.\");\n out(String(ddlContent));\n out(\"\\n✅ PostgreSQL Policies DDL generated successfully.\");\n out(String(policiesContent));\n }\n } catch (error) {\n outError(`Error generating DDL schema: ${describeFailure(error)}`);\n // Step 1 of `rebase db push` is this, and a failure has to stop it.\n // Going on made a resources module that does not evaluate — the single\n // most common cause here — report `✓ rebase db push completed\n // successfully` while Atlas applied whatever `drizzle/schema.sql`\n // happened to be on disk, sometimes minutes old. With no prior file the\n // same run died inside Atlas on `stat drizzle/schema.sql: no such file\n // or directory`, which hides the real cause two screens up.\n throw new GenerationFailed(\"Postgres DDL generation failed\", { cause: error });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,WAAW,SAAyB,MAAM,KAAK,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;AAkBtE,IAAa,8BAAsC;CAC/C,IAAI,QAAQ,IAAI,6BAA6B,WACzC,OAAO,oBAAoB,QAAQ,YAAY,EAAE;CAGrD,OAAO,mBAAmB,QAAQ,oBAAoB,EAAE;AAC5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACPA,IAAa,mBAAb,cAAsC,MAAM;CACxC,YAAY,SAAiB,SAA+B;EACxD,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CAChB;AACJ;;AAgBA,eAAe,sBAAsB,iBAAsD;CAGvF,MAAM,SAAS,MAAM,6BAA6B,KAAK,QAAQ,eAAe,CAAC;CAG/E,MAAM,cAAkC,MAAM,QAAQ,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC;CAC/E,YAAY,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;CACvD,OAAO;AACX;;AAGA,eAAsB,mBAAmB,SAA6C;CAClF,IAAI;EAEA,MAAM,gBAAgB,eAAe,MADX,sBAAsB,QAAQ,eAAe,CACvB;EAEhD,IAAI,QAAQ,YAAY;GACpB,MAAM,SAAW,MAAM,KAAK,QAAQ,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;GAC5E,MAAM,SAAW,UAAU,QAAQ,YAAY,aAAa;GAC5D,IAAI,8CAA8C,QAAQ,YAAY;EAC1E,OAAO;GACH,IAAI,0CAA0C;GAC9C,IAAI,OAAO,aAAa,CAAC;EAC7B;EAEA,IAAI,QAAQ,aAAa,OAAO,IAAI,sBAAsB,CAAC;CAC/D,SAAS,OAAO;EACZ,SAAS,4BAA4B,gBAAgB,KAAK,GAAG;EAG7D,MAAM,IAAI,iBAAiB,oCAAoC,EAAE,OAAO,MAAM,CAAC;CACnF;AACJ;;AAGA,IAAM,mBAAmB;CAAC;CAAgB;CAAgB;CAAsB;AAAoB;;;;;;;;;;;;;;;;;;;;AAqBpG,eAAe,sBAAsB,WAAkC;CACnE,MAAM,QAAQ,iBACT,KAAI,SAAQ,KAAK,KAAK,WAAW,IAAI,CAAC,CAAC,CACvC,MAAK,cAAa,KAAG,WAAW,SAAS,CAAC;CAC/C,IAAI,CAAC,OAAO;CACZ,MAAM,OAAO,cAAc,KAAK,CAAC,CAAC;AACtC;;;;;AAMA,eAAsB,iBAAiB,SAA6C;CAChF,IAAI;EACA,MAAM,eAAe,KAAK,QAAQ,QAAQ,eAAe;EAKzD,MAAM,sBAAsB,KAAK,QAAQ,YAAY,CAAC;EAEtD,MAAM,cAAc,MAAM,sBAAsB,YAAY;EAM5D,MAAM,aAAa,oBAAoB,aAAa;GAChD,iBAAiB;GACjB,eAAe;GACf,eAAe;EACnB,CAAC;EACD,MAAM,kBAAkB,4BAA4B,WAAW;EAC/D,MAAM,gBAAgB,0BAA0B,WAAW;EAC3D,MAAM,gBAAgB,0BAA0B,aAAa,EACzD,YAAY,2BAA2B,EAC3C,CAAC;EACD,MAAM,kBAAkB,4BAA4B,WAAW;EAE/D,MAAM,aAAa,QAAQ;EAC3B,IAAI,YAAY;GACZ,MAAM,YAAY,KAAK,QAAQ,UAAU;GACzC,MAAM,SAAW,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;GACrD,MAAM,SAAW,UAAU,YAAY,UAAU;GACjD,IAAI,8CAA8C,YAAY;GAE9D,MAAM,eAAe,KAAK,KAAK,WAAW,cAAc;GACxD,MAAM,SAAW,UAAU,cAAc,eAAe;GACxD,IAAI,uDAAuD,cAAc;GAKzE,MAAM,aAAa,KAAK,KAAK,WAAW,YAAY;GACpD,IAAI,eAAe;IACf,MAAM,SAAW,UAAU,YAAY,aAAa;IACpD,IAAI,qDAAqD,YAAY;GACzE,OAAO,IAAI,KAAG,WAAW,UAAU,GAC/B,MAAM,SAAW,GAAG,UAAU;GAQlC,MAAM,aAAa,KAAK,KAAK,WAAW,YAAY;GACpD,IAAI,eAAe;IACf,MAAM,SAAW,UAAU,YAAY,aAAa;IACpD,IAAI,qDAAqD,YAAY;GACzE,OAAO,IAAI,KAAG,WAAW,UAAU,GAC/B,MAAM,SAAW,GAAG,UAAU;GAOlC,MAAM,eAAe,KAAK,KAAK,WAAW,cAAc;GACxD,IAAI,iBAAiB;IACjB,MAAM,SAAW,UAAU,cAAc,eAAe;IACxD,IAAI,sDAAsD,cAAc;GAC5E,OAAO,IAAI,KAAG,WAAW,YAAY,GACjC,MAAM,SAAW,GAAG,YAAY;EAExC,OAAO;GACH,IAAI,0CAA0C;GAC9C,IAAI,OAAO,UAAU,CAAC;GACtB,IAAI,qDAAqD;GACzD,IAAI,OAAO,eAAe,CAAC;EAC/B;CACJ,SAAS,OAAO;EACZ,SAAS,gCAAgC,gBAAgB,KAAK,GAAG;EAQjE,MAAM,IAAI,iBAAiB,kCAAkC,EAAE,OAAO,MAAM,CAAC;CACjF;AACJ"}
@@ -1,8 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
- import { n as planSchema } from "./plan-schema-DU9exq6C.js";
5
- import { a as renderPolicyStatements, c as renderSearchDdl, d as searchExcludePatternsOf, f as triggerExcludePatternsOf, i as renderPoliciesDdl, l as renderTriggersDdl, o as renderPostgresDdl, p as vectorExcludePatternsOf, r as renderPgType, u as renderVectorDdl } from "./render-ddl-Ds2t_d9V.js";
4
+ import { n as planSchema } from "./plan-schema-B1YF_tYq.js";
5
+ import { a as renderPolicyStatements, c as renderSearchDdl, d as searchExcludePatternsOf, f as triggerExcludePatternsOf, i as renderPoliciesDdl, l as renderTriggersDdl, o as renderPostgresDdl, p as vectorExcludePatternsOf, r as renderPgType, u as renderVectorDdl } from "./render-ddl-CtC3y9Ol.js";
6
6
  import "@rebasepro/types";
7
7
  import "@rebasepro/common";
8
8
  //#region src/schema/generate-postgres-ddl-logic.ts
@@ -149,4 +149,4 @@ var searchColumnDependencies = (allCollections) => {
149
149
  //#endregion
150
150
  export { generatePostgresVectorDdl as a, planJunctionTables as c, generatePostgresTriggersDdl as i, generatePostgresPoliciesDdl as n, generate_postgres_ddl_logic_exports as o, generatePostgresSearchDdl as r, planCollectionPolicies as s, generatePostgresDdl as t };
151
151
 
152
- //# sourceMappingURL=generate-postgres-ddl-logic-BJsLaVNX.js.map
152
+ //# sourceMappingURL=generate-postgres-ddl-logic-B64O2oZF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-postgres-ddl-logic-BJsLaVNX.js","names":[],"sources":["../src/schema/generate-postgres-ddl-logic.ts"],"sourcesContent":["/**\n * The SQL half of `rebase db generate`, as a plan and a renderer.\n *\n * There is no interpretation left in this file. `schema/plan/plan-schema.ts`\n * reads the collections into a {@link SchemaPlan} and `schema/plan/render-ddl.ts`\n * writes the files; what remains here is the public entry points those two are\n * reached through, plus the smaller plans (`planRelationalColumns`,\n * `planJunctionTables`, `planCollectionPolicies`) that other modules ask for as\n * slices of the same plan.\n *\n * It used to hold `getSqlColumnType` and a `CREATE TABLE` walk with its own\n * reading of relations, junctions, enums and search — one of three such\n * readings. See `schema/plan/types.ts` for what that cost.\n */\nimport { CollectionConfig, SecurityRule, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { planSchema, compileSecurityRule, quoteSqlLiteral, defaultBelongsToOnDelete } from \"./plan/plan-schema\";\nimport {\n renderPgType,\n renderPoliciesDdl,\n renderPolicyStatements,\n renderPostgresDdl,\n renderSearchDdl,\n renderTriggersDdl,\n renderVectorDdl,\n searchExcludePatternsOf,\n triggerExcludePatternsOf,\n vectorExcludePatternsOf,\n type DdlRenderOptions\n} from \"./plan/render-ddl\";\nimport type { ForeignKeyPlan, SchemaPlan } from \"./plan/types\";\nimport type { GeneratedColumnDependency } from \"./generated-column-conflicts\";\n\nexport { quoteSqlLiteral, defaultBelongsToOnDelete };\nexport type { ForeignKeyPlan };\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\n/**\n * The individual SQL statements a single security rule compiles to: a\n * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete\n * statement (terminated by `;`, no trailing newline).\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const generatePolicyStatements = (\n collection: CollectionConfig,\n rule: SecurityRule,\n resolveCollection: ResolveCollection\n): string[] => {\n const table = {\n schema: isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\",\n table: getTableName(collection)\n };\n return compileSecurityRule(collection, rule, resolveCollection, false)\n .flatMap(policy => renderPolicyStatements(table, policy));\n};\n\n/**\n * `.rebase/sql/schema.sql` — the desired state Atlas diffs the database against.\n *\n * Synchronous. It was `async` with nothing to await, which made every caller\n * (the writer script, the doctor, the live editor, two gates) carry a promise\n * for no reason and made the one place that forgot to await it silently compare\n * `[object Promise]`.\n */\nexport const generatePostgresDdl = (\n allCollections: CollectionConfig[],\n options: DdlRenderOptions = {}\n): string => renderPostgresDdl(planSchema(allCollections), options);\n\n/** `.rebase/sql/policies.sql` — the RLS policies. Atlas has no model for a policy. */\nexport const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string =>\n renderPoliciesDdl(planSchema(allCollections));\n\n/**\n * `.rebase/sql/search.sql` — everything a `search` block needs, as a file Rebase\n * applies itself.\n *\n * Search is one of the parts of the schema Atlas does not own, for two\n * independent reasons and either alone would be enough:\n *\n * 1. Its free tier refuses to *parse* a desired-state file that so much as\n * contains a function — \"functions and procedures are available to\n * logged-in users only\". A generated `tsvector` column cannot avoid one:\n * `unaccent` is STABLE and jsonb flattening needs a set-returning function,\n * so both have to be wrapped in an IMMUTABLE helper to be legal in a\n * generated column at all.\n * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs\n * against, so a helper seeded there beforehand is gone by the time the plan\n * is analysed. There is no hook to reinstate it.\n *\n * Empty when nothing opted in — the caller writes no file then.\n */\nexport const generatePostgresSearchDdl = (allCollections: CollectionConfig[]): string =>\n renderSearchDdl(planSchema(allCollections));\n\n/**\n * Glob patterns telling Atlas to leave the search column and its index alone.\n *\n * Without these, a desired state that omits search reads to Atlas as an\n * instruction to drop the column — taking the index and the whole search\n * feature with it on the next push.\n */\nexport const searchExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n searchExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/vector.sql` — the `vector` columns, their ANN indexes and, when a\n * database declared it, the extension.\n *\n * Vector is out of Atlas's hands for a narrower reason than search: not that\n * Atlas refuses to parse the file, but that it cannot *execute* it. To compute\n * a desired state Atlas materialises `schema.sql` in a dev database, that\n * database is created empty, and Atlas empties it again at the start of every\n * run — so `VECTOR(384)` resolves against a database that structurally cannot\n * have pgvector, and every push dies with `type \"vector\" does not exist`.\n */\nexport const generatePostgresVectorDdl = (\n allCollections: CollectionConfig[],\n options: { extensions?: readonly string[] } = {}\n): string => renderVectorDdl(planSchema(allCollections, { databaseExtensions: options.extensions }));\n\n/** @see searchExcludePatterns */\nexport const vectorExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n vectorExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/triggers.sql` — the `BEFORE UPDATE` triggers behind\n * `autoValue: \"on_update\"`, and the one function they share.\n *\n * A trigger is a function plus a binding, so it is carved out of Atlas's view\n * for the first of the two reasons search is. Empty when no property declares\n * `on_update` — the caller writes no file then, and removes a stale one.\n */\nexport const generatePostgresTriggersDdl = (allCollections: CollectionConfig[]): string =>\n renderTriggersDdl(planSchema(allCollections));\n\n/** @see searchExcludePatterns */\nexport const triggerExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n triggerExcludePatternsOf(planSchema(allCollections));\n\n// ── Slices of the plan other modules ask for ─────────────────────────────────\n\n/** A column a `relation` or `reference` property owns on its own table. */\nexport interface RelationalColumnPlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n /** Postgres type, exactly as the DDL renderer declares it. */\n type: string;\n /** `validation.required` on the property that owns the link. */\n required: boolean;\n /**\n * True when a declared property emits this column and the relation\n * contributes only the constraint — `postId` with `columnName: \"post_id\"`\n * beside a `belongsTo` on `post_id`.\n */\n columnOwnedByProperty?: boolean;\n /** Absent when the target collection is not part of this bundle. */\n foreignKey?: ForeignKeyPlan;\n /**\n * What this column would have been called before `generateForeignKeyName`\n * learned to singularize. Carried so the boot-time ensure can notice a\n * database provisioned under the old rule; never used to name anything.\n */\n legacyColumn?: string;\n}\n\n/** The table behind a many-to-many `through` relation. */\nexport interface JunctionTablePlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n columns: { name: string; type: string; legacyName?: string }[];\n /** Both endpoint columns plus the composite primary key. */\n createTable: string;\n foreignKeys: ForeignKeyPlan[];\n}\n\nconst planOf = (collections: CollectionConfig[]): SchemaPlan => planSchema(collections);\n\n/**\n * The FK columns the declared collections own — one entry per `relation`\n * (`belongsTo` side) or `reference` property.\n *\n * A view over the schema plan. It was a second walk of the collections beside\n * the `CREATE TABLE` one, which is how boot-ensure came to add every relation\n * column bare while `db push` made the same column `NOT NULL`.\n */\nexport const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {\n const plan = planOf(allCollections);\n const out: RelationalColumnPlan[] = [];\n for (const table of plan.tables) {\n if (table.kind !== \"collection\") continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n out.push({\n schema: table.schema,\n table: table.table,\n column: column.column,\n type: renderPgType(column.type),\n required: !column.nullable,\n columnOwnedByProperty: column.columnOwnedByProperty,\n legacyColumn: column.legacyColumn,\n foreignKey: column.foreignKey\n });\n }\n }\n return out;\n};\n\n/**\n * The junction tables a bundle's many-to-many relations imply.\n *\n * Derived from the same plan the derived RLS comes from, so a table created\n * here always has policies planned for it — a junction with row-level security\n * left off is readable and writable by every signed-in user.\n */\nexport const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] =>\n planOf(allCollections).tables\n .filter(table => table.kind === \"junction\")\n .map(table => ({\n schema: table.schema,\n table: table.table,\n columns: table.columns.map(c => ({\n name: c.column,\n type: renderPgType(c.type),\n legacyName: c.legacyColumn\n })),\n createTable:\n `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (` +\n table.columns.map(c => `\"${c.column}\" ${renderPgType(c.type)} NOT NULL`).join(\", \") +\n `, PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")}));`,\n foreignKeys: table.columns.map(c => c.foreignKey).filter((fk): fk is ForeignKeyPlan => fk !== undefined)\n }));\n\nexport interface CollectionPolicyPlan {\n /** The table's schema (e.g. `public`, `rebase`). */\n schema: string;\n /** The bare table name, no schema prefix. */\n table: string;\n /** `schema.table` — matches the keys `readExistingSchema` returns. */\n qualified: string;\n /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */\n enableRls: string;\n /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */\n policyStatements: string[];\n}\n\n/**\n * The per-table RLS plan for the declared collections and their junctions, as\n * executable statements — what the managed runtime applies at boot so a freshly\n * provisioned tenant database serves data instead of 401ing every read.\n *\n * The same policies `policies.sql` carries, from the same plan, so boot and\n * `db push` produce identical RLS from identical collections.\n */\nexport const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {\n const plan = planOf(allCollections);\n // Collections first, then the junctions no collection declares.\n const ordered = [\n ...plan.tables.filter(t => t.kind === \"collection\"),\n ...plan.tables.filter(t => t.kind === \"junction\")\n ];\n return ordered.map(table => ({\n schema: table.schema,\n table: table.table,\n qualified: table.qualified,\n enableRls: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements: table.policies.flatMap(policy => renderPolicyStatements(table, policy))\n }));\n};\n\n/**\n * The `generated column → column it reads` edges a project's `search` blocks\n * imply, derived from the collections alone.\n *\n * The same edges `queryGeneratedColumnDependencies` reads out of `pg_depend`,\n * but available where no database is: `rebase db generate` writes a migration\n * that will run somewhere else, later, against a database it cannot inspect.\n * A spec's `fields[].column` is the physical column each search field starts\n * at, which is exactly what the generated expression depends on.\n */\nexport const searchColumnDependencies = (\n allCollections: CollectionConfig[]\n): GeneratedColumnDependency[] => {\n const dependencies: GeneratedColumnDependency[] = [];\n for (const table of planOf(allCollections).tables) {\n const spec = table.search;\n if (!spec) continue;\n const generated = [spec.column, ...(spec.fuzzy ? [spec.fuzzy.column] : [])];\n const sources = new Set(spec.fields.map(field => field.column));\n for (const column of generated) {\n for (const dependsOn of sources) {\n dependencies.push({ schema: table.schema, table: table.table, column, dependsOn });\n }\n }\n }\n return dependencies;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,IAAa,uBACT,gBACA,UAA4B,CAAC,MACpB,kBAAkB,WAAW,cAAc,GAAG,OAAO;;AAGlE,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;AAqBhD,IAAa,6BAA6B,mBACtC,gBAAgB,WAAW,cAAc,CAAC;;;;;;;;AAS9C,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;;;;AAatD,IAAa,6BACT,gBACA,UAA8C,CAAC,MACtC,gBAAgB,WAAW,gBAAgB,EAAE,oBAAoB,QAAQ,WAAW,CAAC,CAAC;;AAGnG,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;AAUtD,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;AAGhD,IAAa,0BAA0B,mBACnC,yBAAyB,WAAW,cAAc,CAAC;AAyCvD,IAAM,UAAU,gBAAgD,WAAW,WAAW;;;;;;;;AAuCtF,IAAa,sBAAsB,mBAC/B,OAAO,cAAc,CAAC,CAAC,OAClB,QAAO,UAAS,MAAM,SAAS,UAAU,CAAC,CAC1C,KAAI,WAAU;CACX,QAAQ,MAAM;CACd,OAAO,MAAM;CACb,SAAS,MAAM,QAAQ,KAAI,OAAM;EAC7B,MAAM,EAAE;EACR,MAAM,aAAa,EAAE,IAAI;EACzB,YAAY,EAAE;CAClB,EAAE;CACF,aACI,+BAA+B,MAAM,OAAO,KAAK,MAAM,MAAM,OAC7D,MAAM,QAAQ,KAAI,MAAK,IAAI,EAAE,OAAO,IAAI,aAAa,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,IAAI,IAClF,kBAAkB,MAAM,WAAW,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;CACrE,aAAa,MAAM,QAAQ,KAAI,MAAK,EAAE,UAAU,CAAC,CAAC,QAAQ,OAA6B,OAAO,KAAA,CAAS;AAC3G,EAAE;;;;;;;;;AAuBV,IAAa,0BAA0B,mBAA+D;CAClG,MAAM,OAAO,OAAO,cAAc;CAMlC,OAAO,CAHH,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,YAAY,GAClD,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,UAAU,CAE7C,CAAA,CAAQ,KAAI,WAAU;EACzB,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,gBAAgB,MAAM,OAAO,KAAK,MAAM,MAAM;EACzD,kBAAkB,MAAM,SAAS,SAAQ,WAAU,uBAAuB,OAAO,MAAM,CAAC;CAC5F,EAAE;AACN;;;;;;;;;;;AAYA,IAAa,4BACT,mBAC8B;CAC9B,MAAM,eAA4C,CAAC;CACnD,KAAK,MAAM,SAAS,OAAO,cAAc,CAAC,CAAC,QAAQ;EAC/C,MAAM,OAAO,MAAM;EACnB,IAAI,CAAC,MAAM;EACX,MAAM,YAAY,CAAC,KAAK,QAAQ,GAAI,KAAK,QAAQ,CAAC,KAAK,MAAM,MAAM,IAAI,CAAC,CAAE;EAC1E,MAAM,UAAU,IAAI,IAAI,KAAK,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC;EAC9D,KAAK,MAAM,UAAU,WACjB,KAAK,MAAM,aAAa,SACpB,aAAa,KAAK;GAAE,QAAQ,MAAM;GAAQ,OAAO,MAAM;GAAO;GAAQ;EAAU,CAAC;CAG7F;CACA,OAAO;AACX"}
1
+ {"version":3,"file":"generate-postgres-ddl-logic-B64O2oZF.js","names":[],"sources":["../src/schema/generate-postgres-ddl-logic.ts"],"sourcesContent":["/**\n * The SQL half of `rebase db generate`, as a plan and a renderer.\n *\n * There is no interpretation left in this file. `schema/plan/plan-schema.ts`\n * reads the collections into a {@link SchemaPlan} and `schema/plan/render-ddl.ts`\n * writes the files; what remains here is the public entry points those two are\n * reached through, plus the smaller plans (`planRelationalColumns`,\n * `planJunctionTables`, `planCollectionPolicies`) that other modules ask for as\n * slices of the same plan.\n *\n * It used to hold `getSqlColumnType` and a `CREATE TABLE` walk with its own\n * reading of relations, junctions, enums and search — one of three such\n * readings. See `schema/plan/types.ts` for what that cost.\n */\nimport { CollectionConfig, SecurityRule, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { planSchema, compileSecurityRule, quoteSqlLiteral, defaultBelongsToOnDelete } from \"./plan/plan-schema\";\nimport {\n renderPgType,\n renderPoliciesDdl,\n renderPolicyStatements,\n renderPostgresDdl,\n renderSearchDdl,\n renderTriggersDdl,\n renderVectorDdl,\n searchExcludePatternsOf,\n triggerExcludePatternsOf,\n vectorExcludePatternsOf,\n type DdlRenderOptions\n} from \"./plan/render-ddl\";\nimport type { ForeignKeyPlan, SchemaPlan } from \"./plan/types\";\nimport type { GeneratedColumnDependency } from \"./generated-column-conflicts\";\n\nexport { quoteSqlLiteral, defaultBelongsToOnDelete };\nexport type { ForeignKeyPlan };\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\n/**\n * The individual SQL statements a single security rule compiles to: a\n * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete\n * statement (terminated by `;`, no trailing newline).\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const generatePolicyStatements = (\n collection: CollectionConfig,\n rule: SecurityRule,\n resolveCollection: ResolveCollection\n): string[] => {\n const table = {\n schema: isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\",\n table: getTableName(collection)\n };\n return compileSecurityRule(collection, rule, resolveCollection, false)\n .flatMap(policy => renderPolicyStatements(table, policy));\n};\n\n/**\n * `.rebase/sql/schema.sql` — the desired state Atlas diffs the database against.\n *\n * Synchronous. It was `async` with nothing to await, which made every caller\n * (the writer script, the doctor, the live editor, two gates) carry a promise\n * for no reason and made the one place that forgot to await it silently compare\n * `[object Promise]`.\n */\nexport const generatePostgresDdl = (\n allCollections: CollectionConfig[],\n options: DdlRenderOptions = {}\n): string => renderPostgresDdl(planSchema(allCollections), options);\n\n/** `.rebase/sql/policies.sql` — the RLS policies. Atlas has no model for a policy. */\nexport const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string =>\n renderPoliciesDdl(planSchema(allCollections));\n\n/**\n * `.rebase/sql/search.sql` — everything a `search` block needs, as a file Rebase\n * applies itself.\n *\n * Search is one of the parts of the schema Atlas does not own, for two\n * independent reasons and either alone would be enough:\n *\n * 1. Its free tier refuses to *parse* a desired-state file that so much as\n * contains a function — \"functions and procedures are available to\n * logged-in users only\". A generated `tsvector` column cannot avoid one:\n * `unaccent` is STABLE and jsonb flattening needs a set-returning function,\n * so both have to be wrapped in an IMMUTABLE helper to be legal in a\n * generated column at all.\n * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs\n * against, so a helper seeded there beforehand is gone by the time the plan\n * is analysed. There is no hook to reinstate it.\n *\n * Empty when nothing opted in — the caller writes no file then.\n */\nexport const generatePostgresSearchDdl = (allCollections: CollectionConfig[]): string =>\n renderSearchDdl(planSchema(allCollections));\n\n/**\n * Glob patterns telling Atlas to leave the search column and its index alone.\n *\n * Without these, a desired state that omits search reads to Atlas as an\n * instruction to drop the column — taking the index and the whole search\n * feature with it on the next push.\n */\nexport const searchExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n searchExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/vector.sql` — the `vector` columns, their ANN indexes and, when a\n * database declared it, the extension.\n *\n * Vector is out of Atlas's hands for a narrower reason than search: not that\n * Atlas refuses to parse the file, but that it cannot *execute* it. To compute\n * a desired state Atlas materialises `schema.sql` in a dev database, that\n * database is created empty, and Atlas empties it again at the start of every\n * run — so `VECTOR(384)` resolves against a database that structurally cannot\n * have pgvector, and every push dies with `type \"vector\" does not exist`.\n */\nexport const generatePostgresVectorDdl = (\n allCollections: CollectionConfig[],\n options: { extensions?: readonly string[] } = {}\n): string => renderVectorDdl(planSchema(allCollections, { databaseExtensions: options.extensions }));\n\n/** @see searchExcludePatterns */\nexport const vectorExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n vectorExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/triggers.sql` — the `BEFORE UPDATE` triggers behind\n * `autoValue: \"on_update\"`, and the one function they share.\n *\n * A trigger is a function plus a binding, so it is carved out of Atlas's view\n * for the first of the two reasons search is. Empty when no property declares\n * `on_update` — the caller writes no file then, and removes a stale one.\n */\nexport const generatePostgresTriggersDdl = (allCollections: CollectionConfig[]): string =>\n renderTriggersDdl(planSchema(allCollections));\n\n/** @see searchExcludePatterns */\nexport const triggerExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n triggerExcludePatternsOf(planSchema(allCollections));\n\n// ── Slices of the plan other modules ask for ─────────────────────────────────\n\n/** A column a `relation` or `reference` property owns on its own table. */\nexport interface RelationalColumnPlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n /** Postgres type, exactly as the DDL renderer declares it. */\n type: string;\n /** `validation.required` on the property that owns the link. */\n required: boolean;\n /**\n * True when a declared property emits this column and the relation\n * contributes only the constraint — `postId` with `columnName: \"post_id\"`\n * beside a `belongsTo` on `post_id`.\n */\n columnOwnedByProperty?: boolean;\n /** Absent when the target collection is not part of this bundle. */\n foreignKey?: ForeignKeyPlan;\n /**\n * What this column would have been called before `generateForeignKeyName`\n * learned to singularize. Carried so the boot-time ensure can notice a\n * database provisioned under the old rule; never used to name anything.\n */\n legacyColumn?: string;\n}\n\n/** The table behind a many-to-many `through` relation. */\nexport interface JunctionTablePlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n columns: { name: string; type: string; legacyName?: string }[];\n /** Both endpoint columns plus the composite primary key. */\n createTable: string;\n foreignKeys: ForeignKeyPlan[];\n}\n\nconst planOf = (collections: CollectionConfig[]): SchemaPlan => planSchema(collections);\n\n/**\n * The FK columns the declared collections own — one entry per `relation`\n * (`belongsTo` side) or `reference` property.\n *\n * A view over the schema plan. It was a second walk of the collections beside\n * the `CREATE TABLE` one, which is how boot-ensure came to add every relation\n * column bare while `db push` made the same column `NOT NULL`.\n */\nexport const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {\n const plan = planOf(allCollections);\n const out: RelationalColumnPlan[] = [];\n for (const table of plan.tables) {\n if (table.kind !== \"collection\") continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n out.push({\n schema: table.schema,\n table: table.table,\n column: column.column,\n type: renderPgType(column.type),\n required: !column.nullable,\n columnOwnedByProperty: column.columnOwnedByProperty,\n legacyColumn: column.legacyColumn,\n foreignKey: column.foreignKey\n });\n }\n }\n return out;\n};\n\n/**\n * The junction tables a bundle's many-to-many relations imply.\n *\n * Derived from the same plan the derived RLS comes from, so a table created\n * here always has policies planned for it — a junction with row-level security\n * left off is readable and writable by every signed-in user.\n */\nexport const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] =>\n planOf(allCollections).tables\n .filter(table => table.kind === \"junction\")\n .map(table => ({\n schema: table.schema,\n table: table.table,\n columns: table.columns.map(c => ({\n name: c.column,\n type: renderPgType(c.type),\n legacyName: c.legacyColumn\n })),\n createTable:\n `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (` +\n table.columns.map(c => `\"${c.column}\" ${renderPgType(c.type)} NOT NULL`).join(\", \") +\n `, PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")}));`,\n foreignKeys: table.columns.map(c => c.foreignKey).filter((fk): fk is ForeignKeyPlan => fk !== undefined)\n }));\n\nexport interface CollectionPolicyPlan {\n /** The table's schema (e.g. `public`, `rebase`). */\n schema: string;\n /** The bare table name, no schema prefix. */\n table: string;\n /** `schema.table` — matches the keys `readExistingSchema` returns. */\n qualified: string;\n /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */\n enableRls: string;\n /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */\n policyStatements: string[];\n}\n\n/**\n * The per-table RLS plan for the declared collections and their junctions, as\n * executable statements — what the managed runtime applies at boot so a freshly\n * provisioned tenant database serves data instead of 401ing every read.\n *\n * The same policies `policies.sql` carries, from the same plan, so boot and\n * `db push` produce identical RLS from identical collections.\n */\nexport const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {\n const plan = planOf(allCollections);\n // Collections first, then the junctions no collection declares.\n const ordered = [\n ...plan.tables.filter(t => t.kind === \"collection\"),\n ...plan.tables.filter(t => t.kind === \"junction\")\n ];\n return ordered.map(table => ({\n schema: table.schema,\n table: table.table,\n qualified: table.qualified,\n enableRls: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements: table.policies.flatMap(policy => renderPolicyStatements(table, policy))\n }));\n};\n\n/**\n * The `generated column → column it reads` edges a project's `search` blocks\n * imply, derived from the collections alone.\n *\n * The same edges `queryGeneratedColumnDependencies` reads out of `pg_depend`,\n * but available where no database is: `rebase db generate` writes a migration\n * that will run somewhere else, later, against a database it cannot inspect.\n * A spec's `fields[].column` is the physical column each search field starts\n * at, which is exactly what the generated expression depends on.\n */\nexport const searchColumnDependencies = (\n allCollections: CollectionConfig[]\n): GeneratedColumnDependency[] => {\n const dependencies: GeneratedColumnDependency[] = [];\n for (const table of planOf(allCollections).tables) {\n const spec = table.search;\n if (!spec) continue;\n const generated = [spec.column, ...(spec.fuzzy ? [spec.fuzzy.column] : [])];\n const sources = new Set(spec.fields.map(field => field.column));\n for (const column of generated) {\n for (const dependsOn of sources) {\n dependencies.push({ schema: table.schema, table: table.table, column, dependsOn });\n }\n }\n }\n return dependencies;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,IAAa,uBACT,gBACA,UAA4B,CAAC,MACpB,kBAAkB,WAAW,cAAc,GAAG,OAAO;;AAGlE,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;AAqBhD,IAAa,6BAA6B,mBACtC,gBAAgB,WAAW,cAAc,CAAC;;;;;;;;AAS9C,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;;;;AAatD,IAAa,6BACT,gBACA,UAA8C,CAAC,MACtC,gBAAgB,WAAW,gBAAgB,EAAE,oBAAoB,QAAQ,WAAW,CAAC,CAAC;;AAGnG,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;AAUtD,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;AAGhD,IAAa,0BAA0B,mBACnC,yBAAyB,WAAW,cAAc,CAAC;AAyCvD,IAAM,UAAU,gBAAgD,WAAW,WAAW;;;;;;;;AAuCtF,IAAa,sBAAsB,mBAC/B,OAAO,cAAc,CAAC,CAAC,OAClB,QAAO,UAAS,MAAM,SAAS,UAAU,CAAC,CAC1C,KAAI,WAAU;CACX,QAAQ,MAAM;CACd,OAAO,MAAM;CACb,SAAS,MAAM,QAAQ,KAAI,OAAM;EAC7B,MAAM,EAAE;EACR,MAAM,aAAa,EAAE,IAAI;EACzB,YAAY,EAAE;CAClB,EAAE;CACF,aACI,+BAA+B,MAAM,OAAO,KAAK,MAAM,MAAM,OAC7D,MAAM,QAAQ,KAAI,MAAK,IAAI,EAAE,OAAO,IAAI,aAAa,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,IAAI,IAClF,kBAAkB,MAAM,WAAW,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;CACrE,aAAa,MAAM,QAAQ,KAAI,MAAK,EAAE,UAAU,CAAC,CAAC,QAAQ,OAA6B,OAAO,KAAA,CAAS;AAC3G,EAAE;;;;;;;;;AAuBV,IAAa,0BAA0B,mBAA+D;CAClG,MAAM,OAAO,OAAO,cAAc;CAMlC,OAAO,CAHH,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,YAAY,GAClD,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,UAAU,CAE7C,CAAA,CAAQ,KAAI,WAAU;EACzB,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,gBAAgB,MAAM,OAAO,KAAK,MAAM,MAAM;EACzD,kBAAkB,MAAM,SAAS,SAAQ,WAAU,uBAAuB,OAAO,MAAM,CAAC;CAC5F,EAAE;AACN;;;;;;;;;;;AAYA,IAAa,4BACT,mBAC8B;CAC9B,MAAM,eAA4C,CAAC;CACnD,KAAK,MAAM,SAAS,OAAO,cAAc,CAAC,CAAC,QAAQ;EAC/C,MAAM,OAAO,MAAM;EACnB,IAAI,CAAC,MAAM;EACX,MAAM,YAAY,CAAC,KAAK,QAAQ,GAAI,KAAK,QAAQ,CAAC,KAAK,MAAM,MAAM,IAAI,CAAC,CAAE;EAC1E,MAAM,UAAU,IAAI,IAAI,KAAK,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC;EAC9D,KAAK,MAAM,UAAU,WACjB,KAAK,MAAM,aAAa,SACpB,aAAa,KAAK;GAAE,QAAQ,MAAM;GAAQ,OAAO,MAAM;GAAO;GAAQ;EAAU,CAAC;CAG7F;CACA,OAAO;AACX"}
@@ -23,6 +23,12 @@ export declare function generatedSqlDir(cwd?: string): string;
23
23
  * behind — and a stale `policies.sql` in the repository is exactly what someone
24
24
  * reviewing the project's RLS would read.
25
25
  *
26
- * @returns the old files it removed, relative to the working directory.
26
+ * With `retire: false` (`db push --dry-run`, which writes nothing of the
27
+ * project's) the old copies are only found, not removed.
28
+ *
29
+ * @returns the old files it removed — or, with `retire: false`, would remove —
30
+ * relative to the working directory.
27
31
  */
28
- export declare function prepareGeneratedSqlDir(cwd?: string): string[];
32
+ export declare function prepareGeneratedSqlDir(cwd?: string, options?: {
33
+ retire?: boolean;
34
+ }): string[];
@@ -2,12 +2,15 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import type { EntityHistoryEntry } from "@rebasepro/types";
3
3
  export type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from "@rebasepro/types";
4
4
  import type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from "@rebasepro/types";
5
+ import { type HistoryTableState } from "./ensure-history-table.js";
5
6
  /**
6
7
  * A Postgres history row is already the wire shape — `updated_at` comes back
7
8
  * from the driver as a string. Kept as an alias because the name is used
8
9
  * throughout this package and in `PostgresBackendDriver`.
9
10
  */
10
11
  export type HistoryEntry = EntityHistoryEntry;
12
+ /** Entries one statement of {@link HistoryService.pruneExpired} deletes at most. */
13
+ export declare const PRUNE_BATCH_SIZE = 5000;
11
14
  /**
12
15
  * Service for recording and querying row change history.
13
16
  * Stores history entries in the `rebase.entity_history` table.
@@ -21,9 +24,14 @@ export declare class HistoryService {
21
24
  * through the pool and commit on their own.
22
25
  */
23
26
  private readonly inTransaction;
24
- constructor(db: NodePgDatabase, retention?: Partial<HistoryRetentionConfig>, options?: {
25
- inTransaction?: boolean;
26
- });
27
+ /**
28
+ * What new entries are stored as — see `EntityHistoryEntry.format`. `2`,
29
+ * the previous values of the changed fields only, needs the table's
30
+ * `format` column, so it is what `ensureHistoryTableExists` found; a
31
+ * service built without saying stores whole rows, as it always did.
32
+ */
33
+ private readonly entryFormat;
34
+ constructor(db: NodePgDatabase, retention?: Partial<HistoryRetentionConfig>, options?: Partial<HistoryTableState>);
27
35
  /**
28
36
  * Record a history entry for a row change.
29
37
  *
@@ -68,15 +76,39 @@ export declare class HistoryService {
68
76
  * Fetch a single history entry by ID.
69
77
  */
70
78
  fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null>;
79
+ /**
80
+ * Each entry's `format`. A table without the column holds only entries
81
+ * that store the whole previous row, which is what `1` says.
82
+ */
83
+ private formatColumn;
71
84
  /**
72
85
  * Prune history for a single row: enforce maxEntries and TTL.
73
86
  */
74
87
  pruneEntity(tableName: string, id: string): Promise<number>;
75
88
  /**
76
- * Global prune: enforce TTL across ALL rows in a single sweep.
77
- * Intended to be called periodically (e.g. once per hour or daily).
89
+ * Delete every entry older than `ttlDays`, whichever row it belongs to.
90
+ *
91
+ * The inline prune above only ever visits a row that was just written,
92
+ * so a row nobody writes again — every deleted one — kept its history for
93
+ * good, whatever `ttlDays` said. This is the other half: the backend runs
94
+ * it once an hour, on one instance of the fleet (see
95
+ * `startHistoryRetentionSweep` in `@rebasepro/server`).
96
+ *
97
+ * In batches of {@link PRUNE_BATCH_SIZE}, each its own statement, repeated
98
+ * until one comes back short: a first sweep over a year of backlog is many
99
+ * short deletes, not one that holds millions of row locks and a pool
100
+ * connection for minutes. `SKIP LOCKED`, so two instances sweeping at once
101
+ * (or a sweep meeting an inline prune) split the work instead of waiting
102
+ * on each other. Each batch reads `idx_history_updated_at`, built in the
103
+ * background at boot (`ensureHistoryIndexes`).
104
+ *
105
+ * `signal` ends it after the batch in flight: a shutdown waits for the
106
+ * sweep, and a first sweep over a large backlog is hundreds of batches.
107
+ * What is left goes in the next sweep.
78
108
  */
79
- pruneExpired(): Promise<number>;
109
+ pruneExpired(options?: {
110
+ signal?: AbortSignal;
111
+ }): Promise<number>;
80
112
  }
81
113
  /**
82
114
  * Shallow comparison to find top-level keys that changed between two objects.
@@ -1,13 +1,51 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import type { DeferredIndexBuild } from "../schema/plan/diff-plan.js";
3
+ import { type BootLockOptions } from "../schema/boot-ddl.js";
2
4
  /** How a history entry gets written from inside the write it records. */
3
5
  export declare const RECORD_HISTORY_FUNCTION = "rebase.record_history";
6
+ /** What {@link ensureHistoryTableExists} left in place, for `HistoryService`. */
7
+ export interface HistoryTableState {
8
+ /**
9
+ * {@link RECORD_HISTORY_FUNCTION} exists, so an entry inside a write goes
10
+ * on that write's transaction. Without it, entries go through the pool and
11
+ * commit on their own.
12
+ */
13
+ inTransaction: boolean;
14
+ /**
15
+ * What new entries are stored as. `2` — an update keeps the previous values
16
+ * of its changed fields only (`EntityHistoryEntry.format`) — once the table
17
+ * has its `format` column; `1`, the whole previous row, where the column
18
+ * could not be added this boot.
19
+ */
20
+ entryFormat: 1 | 2;
21
+ }
22
+ /** Entries by age alone, for the retention sweep (`HistoryService.pruneExpired`). */
23
+ export declare const HISTORY_SWEEP_INDEX = "idx_history_updated_at";
4
24
  /**
5
25
  * Auto-create the row history table if it doesn't exist.
6
26
  * This runs on startup when history is enabled, following the same
7
27
  * pattern as `ensureAuthTablesExist`.
8
28
  *
9
- * Resolves `true` when {@link RECORD_HISTORY_FUNCTION} exists, so an entry can
10
- * be written on the write's own transaction; `false` sends entries through the
11
- * pool as before, committing on their own.
29
+ * Resolves to what the history service may rely on: see {@link HistoryTableState}.
12
30
  */
13
- export declare function ensureHistoryTableExists(db: NodePgDatabase): Promise<boolean>;
31
+ export declare function ensureHistoryTableExists(db: NodePgDatabase, options?: BootLockOptions): Promise<HistoryTableState>;
32
+ /**
33
+ * Bring the history table's indexes to what its reads need, and say which one
34
+ * is left to build in the background.
35
+ *
36
+ * - `idx_history_entity` is dropped: `idx_history_time` leads with the same two
37
+ * columns, so it served nothing the other does not, and cost a btree insert
38
+ * per recorded change. A `DROP INDEX` takes the table's `ACCESS EXCLUSIVE`
39
+ * lock for an instant, so it runs under the boot's lock budget — one second
40
+ * per attempt, a few attempts — and a table something holds the whole time
41
+ * keeps the index until the next boot. Only once `idx_history_time` is there
42
+ * and valid, so a per-row lookup never loses its index.
43
+ * - `idx_history_updated_at`, which the retention sweep reads, is returned for
44
+ * `buildDeferredIndexes`: on a table that already holds years of history the
45
+ * build takes seconds to minutes, so it is built `CONCURRENTLY` after the boot,
46
+ * blocking no read or write. Nothing is returned when it is already valid.
47
+ *
48
+ * Never throws: an index that cannot be dropped or read about is logged, and
49
+ * the next boot asks again.
50
+ */
51
+ export declare function ensureHistoryIndexes(db: NodePgDatabase, options?: BootLockOptions): Promise<DeferredIndexBuild[]>;