@rebasepro/server-postgres 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
@@ -1,436 +0,0 @@
1
- /**
2
- * What a collection change means for a live database.
3
- *
4
- * The live schema editor may only make changes the boot-time ensure path can
5
- * actually carry out, because that is the one mechanism that changes a schema
6
- * (see `ensure-collection-tables.ts`). Its vocabulary is small and deliberately
7
- * so: create a table, add a column, create an enum *type*, create an index, add
8
- * a foreign key, and rename a column via the legacy-name path. There is no
9
- * `ALTER COLUMN TYPE` and no `DROP` of anything.
10
- *
11
- * So this module answers one question per change: **can the ensure path express
12
- * it, and if it can, will the result actually match what the config says?**
13
- *
14
- * ## Three answers, not two
15
- *
16
- * The obvious split is safe / unsafe. It is not enough, because the most
17
- * dangerous case is neither: a change the ensure path *partly* applies, leaving
18
- * a database that does not match the configuration and says nothing about it.
19
- * Two of those exist today and both are documented in the code they come from:
20
- *
21
- * - **A required property added to an existing collection.** `ensure` withholds
22
- * `NOT NULL` on a table that already exists, because the constraint is checked
23
- * against live rows. The column arrives nullable. The config says required;
24
- * the database does not enforce it.
25
- * - **A value added to an existing enum.** `ensure` skips an enum type it
26
- * already sees — `if (existing.enums.has(name)) continue`. The new value never
27
- * reaches the database, and the first insert using it fails.
28
- *
29
- * Both would read as "applied successfully" to anyone watching. Calling them
30
- * `diverges` is the whole point of this module: an editor that reports them as
31
- * safe is worse than one that refuses them.
32
- *
33
- * ## Why refusing is the right default for the rest
34
- *
35
- * Dropping a column, narrowing a type, changing a primary key: each is
36
- * expressible in SQL and none is expressible by `ensure`. They need a migration
37
- * somebody wrote and read. `needs-migration` says exactly that, and naming the
38
- * change is more useful than attempting it.
39
- */
40
- import type {
41
- CollectionConfig,
42
- Property,
43
- SchemaChange,
44
- SchemaChangeKind,
45
- SchemaChangeVerdict,
46
- ClassifiedSchemaChanges
47
- } from "@rebasepro/types";
48
- import { getTableName } from "@rebasepro/common";
49
- import { resolveColumnName } from "./generate-postgres-ddl-logic";
50
-
51
- /**
52
- * The vocabulary lives in `@rebasepro/types` so that `@rebasepro/server`, which
53
- * cannot import this package, can still describe a change. Re-exported here
54
- * under the names this module has always used.
55
- */
56
- export type ChangeVerdict = SchemaChangeVerdict;
57
- export type ChangeKind = SchemaChangeKind;
58
- export type { SchemaChange };
59
- export type ClassifiedChanges = ClassifiedSchemaChanges;
60
-
61
- const VERDICT_RANK: Record<SchemaChangeVerdict, number> = {
62
- safe: 0,
63
- diverges: 1,
64
- "needs-migration": 2
65
- };
66
-
67
- const bySlug = (collections: CollectionConfig[]): Map<string, CollectionConfig> => {
68
- const map = new Map<string, CollectionConfig>();
69
- for (const collection of collections) {
70
- if (collection.slug) map.set(collection.slug, collection);
71
- }
72
- return map;
73
- };
74
-
75
- const propertiesOf = (collection: CollectionConfig): Record<string, Property> =>
76
- (collection.properties ?? {}) as Record<string, Property>;
77
-
78
- /** Enum values a string property declares, or undefined when it is not an enum. */
79
- const enumValuesOf = (prop: Property): string[] | undefined => {
80
- const values = (prop as { enum?: unknown }).enum;
81
- if (!Array.isArray(values)) return undefined;
82
- return values.map(value =>
83
- typeof value === "string" ? value : String((value as { id?: unknown })?.id ?? value)
84
- );
85
- };
86
-
87
- const isRequired = (prop: Property): boolean => prop.validation?.required === true;
88
-
89
- const isIdProperty = (prop: Property): boolean => Boolean((prop as { isId?: unknown }).isId);
90
-
91
- /**
92
- * A change to any of these alters the physical column, which the ensure path
93
- * cannot do. Compared as a tuple so a change to *any* of them is caught without
94
- * this module having to re-derive the SQL type — which is the DDL generator's
95
- * job, and duplicating it here is how the two would drift.
96
- */
97
- const physicalShapeOf = (prop: Property): string => JSON.stringify([
98
- prop.type,
99
- (prop as { columnType?: unknown }).columnType ?? null,
100
- (prop as { dimensions?: unknown }).dimensions ?? null,
101
- (prop as { isId?: unknown }).isId ?? null,
102
- (prop.validation as { max?: unknown } | undefined)?.max ?? null
103
- ]);
104
-
105
- /**
106
- * Facts about the database a change is destined for.
107
- *
108
- * Three of the verdicts below cannot be reached from the collections alone:
109
- * whether a NOT NULL can be added comes down to whether the table holds rows,
110
- * and whether an enum value will land comes down to which values the type
111
- * already has. Without these, the classifier answers conservatively — the
112
- * change *may* diverge — which is the right answer for a caller that has no
113
- * database to look at, and the wrong one to show somebody staring at theirs.
114
- */
115
- export interface SchemaFacts {
116
- /** `schema.table` → columns. */
117
- tables: Map<string, Set<string>>;
118
- /** Tables known to hold at least one row. */
119
- populatedTables?: Set<string>;
120
- /** `schema.table.column` for every column the database marks NOT NULL. */
121
- notNullColumns?: Set<string>;
122
- /** `schema.typename` → the values that type currently holds. */
123
- enumValues?: Map<string, string[]>;
124
- }
125
-
126
- /** Whether the table behind a collection exists and is empty. */
127
- const tableIsEmpty = (collection: CollectionConfig, facts?: SchemaFacts): boolean => {
128
- if (!facts?.populatedTables) return false;
129
- const key = qualifiedTable(collection);
130
- // A table the database does not have yet is one this change creates, and a
131
- // table being created has no rows to check a constraint against.
132
- if (!facts.tables.has(key)) return true;
133
- return !facts.populatedTables.has(key);
134
- };
135
-
136
- const qualifiedTable = (collection: CollectionConfig): string => {
137
- const schema = (collection as { schema?: string }).schema ?? "public";
138
- return `${schema}.${getTableName(collection)}`;
139
- };
140
-
141
- /**
142
- * Classify the difference between two collection sets.
143
- *
144
- * `before` is what the running database was built from; `after` is what the
145
- * editor is proposing. Order within each array is irrelevant.
146
- *
147
- * `facts` is what the database actually looks like. Supplied by the live
148
- * editor; omitted by callers reasoning about collections in the abstract, who
149
- * get the conservative reading.
150
- */
151
- export function classifyCollectionChanges(
152
- before: CollectionConfig[],
153
- after: CollectionConfig[],
154
- facts?: SchemaFacts
155
- ): ClassifiedChanges {
156
- const previous = bySlug(before);
157
- const next = bySlug(after);
158
- const changes: SchemaChange[] = [];
159
-
160
- for (const [slug, collection] of next) {
161
- if (!previous.has(slug)) {
162
- changes.push({
163
- kind: "add-collection",
164
- verdict: "safe",
165
- collection: slug,
166
- detail: `New collection "${slug}" — creates table "${getTableName(collection)}".`
167
- });
168
- continue;
169
- }
170
- classifyProperties(previous.get(slug)!, collection, changes, facts);
171
- }
172
-
173
- for (const [slug, collection] of previous) {
174
- if (next.has(slug)) continue;
175
- changes.push({
176
- kind: "remove-collection",
177
- verdict: "needs-migration",
178
- collection: slug,
179
- detail:
180
- `Collection "${slug}" was removed, which would drop table ` +
181
- `"${getTableName(collection)}" and everything in it.`,
182
- remedy:
183
- "The ensure path never drops anything, so this cannot be applied here. Remove the " +
184
- "collection in a migration you have read, or keep it and stop serving it."
185
- });
186
- }
187
-
188
- const verdict = changes.reduce<ChangeVerdict>(
189
- (worst, change) => (VERDICT_RANK[change.verdict] > VERDICT_RANK[worst] ? change.verdict : worst),
190
- "safe"
191
- );
192
-
193
- return { changes, verdict, applicable: verdict === "safe" };
194
- }
195
-
196
- function classifyProperties(
197
- before: CollectionConfig,
198
- after: CollectionConfig,
199
- changes: SchemaChange[],
200
- facts?: SchemaFacts
201
- ): void {
202
- const slug = after.slug ?? "";
203
- const previous = propertiesOf(before);
204
- const next = propertiesOf(after);
205
- const empty = tableIsEmpty(after, facts);
206
-
207
- for (const [name, prop] of Object.entries(next)) {
208
- const old = previous[name];
209
-
210
- if (!old) {
211
- // A NOT NULL is checked against rows that are already there, so
212
- // whether this is safe is a question about the data, not about the
213
- // configuration. On an empty table the constraint cannot fail and
214
- // the ensure path applies it; on a populated one it is withheld and
215
- // the column arrives nullable, which is a database that disagrees
216
- // with its own config — still worth refusing, now for a reason the
217
- // reader can act on.
218
- if (isRequired(prop) && !empty) {
219
- changes.push({
220
- kind: "add-property",
221
- verdict: "diverges",
222
- collection: slug,
223
- property: name,
224
- detail:
225
- `"${name}" is required, but "${getTableName(after)}" already holds rows, so ` +
226
- "NOT NULL would be checked against data that has no value for it yet. The " +
227
- "column would arrive nullable.",
228
- remedy:
229
- "Add it optional, backfill every row, then make it required — the editor " +
230
- "will apply the constraint once no row violates it."
231
- });
232
- } else {
233
- const column = resolveColumnName(name, prop);
234
- changes.push({
235
- kind: "add-property",
236
- verdict: "safe",
237
- collection: slug,
238
- property: name,
239
- detail: isRequired(prop)
240
- ? `New required property "${name}" — adds column "${column}" NOT NULL, ` +
241
- `which "${getTableName(after)}" can take because it holds no rows.`
242
- : `New optional property "${name}" — adds column "${column}".`
243
- });
244
- }
245
- continue;
246
- }
247
-
248
- classifyProperty(slug, after, name, old, prop, changes, facts);
249
- }
250
-
251
- for (const [name, prop] of Object.entries(previous)) {
252
- if (next[name]) continue;
253
- changes.push({
254
- kind: "remove-property",
255
- verdict: "needs-migration",
256
- collection: slug,
257
- property: name,
258
- detail:
259
- `"${name}" was removed, which would drop column ` +
260
- `"${resolveColumnName(name, prop)}" and its data.`,
261
- remedy:
262
- "The ensure path never drops a column. Remove it in a migration you have read, or " +
263
- "leave the column and stop exposing the property."
264
- });
265
- }
266
- }
267
-
268
- function classifyProperty(
269
- slug: string,
270
- collection: CollectionConfig,
271
- name: string,
272
- before: Property,
273
- after: Property,
274
- changes: SchemaChange[],
275
- facts?: SchemaFacts
276
- ): void {
277
- const beforeColumn = resolveColumnName(name, before);
278
- const afterColumn = resolveColumnName(name, after);
279
-
280
- if (beforeColumn !== afterColumn) {
281
- changes.push({
282
- kind: "rename-column",
283
- verdict: "needs-migration",
284
- collection: slug,
285
- property: name,
286
- detail: `"${name}" changes column from "${beforeColumn}" to "${afterColumn}".`,
287
- remedy:
288
- "The ensure path only renames a column through its legacy-name path, which this is " +
289
- "not. Rename it in a migration, or the old column stays and the new one is created " +
290
- "empty beside it."
291
- });
292
- }
293
-
294
- if (isIdProperty(before) !== isIdProperty(after)) {
295
- changes.push({
296
- kind: "change-primary-key",
297
- verdict: "needs-migration",
298
- collection: slug,
299
- property: name,
300
- detail: `"${name}" changes whether it is the primary key.`,
301
- remedy: "A primary key change rewrites the table and every foreign key into it. Migration only."
302
- });
303
- return;
304
- }
305
-
306
- if (physicalShapeOf(before) !== physicalShapeOf(after)) {
307
- changes.push({
308
- kind: "change-property-type",
309
- verdict: "needs-migration",
310
- collection: slug,
311
- property: name,
312
- detail: `"${name}" changes physical type — ${before.type} to ${after.type}.`,
313
- remedy:
314
- "There is no ALTER COLUMN TYPE in the ensure path, and a cast can fail on data that " +
315
- "is already there. Change it in a migration."
316
- });
317
- }
318
-
319
- if (!isRequired(before) && isRequired(after)) {
320
- // Tightening. `SET NOT NULL` scans the table, so this comes down to
321
- // whether anything in it is null — which on an empty table is nothing.
322
- const empty = tableIsEmpty(collection, facts);
323
- changes.push(empty
324
- ? {
325
- kind: "change-required",
326
- verdict: "safe",
327
- collection: slug,
328
- property: name,
329
- detail:
330
- `"${name}" became required — sets NOT NULL on "${afterColumn}", which ` +
331
- `"${getTableName(collection)}" can take because it holds no rows.`
332
- }
333
- : {
334
- kind: "change-required",
335
- verdict: "diverges",
336
- collection: slug,
337
- property: name,
338
- detail:
339
- `"${name}" became required, but "${getTableName(collection)}" holds rows and ` +
340
- "SET NOT NULL is checked against every one of them. The database would keep " +
341
- "accepting nulls.",
342
- remedy:
343
- `Backfill first — UPDATE the rows where "${afterColumn}" IS NULL — then apply ` +
344
- "this again."
345
- });
346
- }
347
-
348
- if (isRequired(before) && !isRequired(after)) {
349
- // Relaxing. `DROP NOT NULL` cannot fail and cannot lose data, so this is
350
- // safe on any table; the editor plans it because a reviewed change is
351
- // the one context in which touching an existing column's constraints is
352
- // something somebody asked for.
353
- changes.push({
354
- kind: "change-required",
355
- verdict: "safe",
356
- collection: slug,
357
- property: name,
358
- detail: `"${name}" is no longer required — drops NOT NULL from "${afterColumn}".`
359
- });
360
- }
361
-
362
- classifyEnum(slug, collection, name, before, after, changes, facts);
363
- }
364
-
365
- function classifyEnum(
366
- slug: string,
367
- collection: CollectionConfig,
368
- name: string,
369
- before: Property,
370
- after: Property,
371
- changes: SchemaChange[],
372
- facts?: SchemaFacts
373
- ): void {
374
- const oldValues = enumValuesOf(before);
375
- const newValues = enumValuesOf(after);
376
- if (!oldValues || !newValues) return;
377
-
378
- const added = newValues.filter(value => !oldValues.includes(value));
379
- const removed = oldValues.filter(value => !newValues.includes(value));
380
-
381
- if (added.length > 0) {
382
- // `ADD VALUE` is additive, idempotent with IF NOT EXISTS, and needs no
383
- // table scan, so the ensure path now carries it — but only when it can
384
- // see which values the type already has. A caller with no database
385
- // cannot know that, and for them this is still the change that silently
386
- // does not land.
387
- const seesTheType = facts?.enumValues !== undefined;
388
- changes.push(seesTheType
389
- ? {
390
- kind: "add-enum-value",
391
- verdict: "safe",
392
- collection: slug,
393
- property: name,
394
- detail: `"${name}" gains ${added.map(v => `"${v}"`).join(", ")} — ALTER TYPE … ADD VALUE.`
395
- }
396
- : {
397
- kind: "add-enum-value",
398
- verdict: "diverges",
399
- collection: slug,
400
- property: name,
401
- detail:
402
- `"${name}" gains ${added.map(v => `"${v}"`).join(", ")}, and the values this ` +
403
- "type already has are not known here, so whether they would land cannot be said.",
404
- remedy: "Plan this against the database it is destined for."
405
- });
406
- }
407
-
408
- if (removed.length > 0) {
409
- changes.push({
410
- kind: "remove-enum-value",
411
- verdict: "needs-migration",
412
- collection: slug,
413
- property: name,
414
- detail: `"${name}" drops ${removed.map(v => `"${v}"`).join(", ")}.`,
415
- remedy:
416
- "Postgres cannot remove a value from an enum type. Recreate the type in a migration, " +
417
- "after rewriting every row still using the value."
418
- });
419
- }
420
- }
421
-
422
- /** A one-line summary, for a log or a refusal message. */
423
- export function summarizeChanges(classified: ClassifiedChanges): string {
424
- if (classified.changes.length === 0) return "No schema changes.";
425
-
426
- const counts = classified.changes.reduce<Record<ChangeVerdict, number>>(
427
- (acc, change) => ({ ...acc, [change.verdict]: (acc[change.verdict] ?? 0) + 1 }),
428
- { safe: 0, diverges: 0, "needs-migration": 0 }
429
- );
430
-
431
- const parts = (["needs-migration", "diverges", "safe"] as ChangeVerdict[])
432
- .filter(verdict => counts[verdict] > 0)
433
- .map(verdict => `${counts[verdict]} ${verdict}`);
434
-
435
- return `${classified.changes.length} change(s): ${parts.join(", ")}.`;
436
- }